Skip to content

hollo

v0.9.0 Breaking

This release includes 1 breaking change for platform teams planning a safe upgrade.

Published 14d Communication & Email
✓ No known CVEs patched
Read the diff → Tool health → What is this tool? →

✓ No known CVEs patched in this version

Topics

activitypub fediverse microblog

Affected surfaces

auth breaking_upgrade

Summary

AI summary

Broad release touches https://github.com/fedify-dev/hollo/issues/481, https://github.com/fedify-dev/hollo/pull/483, https://github.com/fedify-dev/hollo/pull/458, and https://github.com/fedify-dev/hollo/issues/67.

Changes in this release

Security High

Fixed bug that accepted ActivityPub posts with timestamps more than 12 hours in the future, preventing timeline manipulation via forged timestamps.

Fixed bug that accepted ActivityPub posts with timestamps more than 12 hours in the future, preventing timeline manipulation via forged timestamps.

Source: granite4.1:30b@2026-05-20-audit

Confidence: high

Feature Medium

Added passkey (WebAuthn) authentication with enrollment and management UI.

Added passkey (WebAuthn) authentication with enrollment and management UI.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: high

Feature Medium

Implemented split-domain WebFinger support via HANDLE_HOST and WEB_ORIGIN env vars.

Implemented split-domain WebFinger support via HANDLE_HOST and WEB_ORIGIN env vars.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: high

Feature Medium

Introduced media proxy rewriting remote avatars, headers, attachments, emojis to Hollo origin.

Introduced media proxy rewriting remote avatars, headers, attachments, emojis to Hollo origin.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: high

Feature Medium

Enabled optional split-domain WebFinger support with HANDLE_HOST and WEB_ORIGIN variables.

Enabled optional split-domain WebFinger support with HANDLE_HOST and WEB_ORIGIN variables.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: high

Feature Medium

Added FEP-044f quote authorization and policy support on Mastodon-compatible quote APIs.

Added FEP-044f quote authorization and policy support on Mastodon-compatible quote APIs.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: high

Feature Medium

Allowed up to 10 custom profile fields per account beyond Mastodon's limit.

Allowed up to 10 custom profile fields per account beyond Mastodon's limit.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: high

Feature Medium

Implemented public Followers and Following pages with pagination and search filter.

Implemented public Followers and Following pages with pagination and search filter.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: high

Feature Medium

Added per-post reaction list pages (/likes, /shares, /reactions/:emoji, /quotes).

Added per-post reaction list pages (/likes, /shares, /reactions/:emoji, /quotes).

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: high

Feature Medium

Switched to PostgreSQL trigram indexes via pg_trgm for faster search queries.

Switched to PostgreSQL trigram indexes via pg_trgm for faster search queries.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: high

Feature Medium

Implemented optional split-domain WebFinger support with HANDLE_HOST and WEB_ORIGIN variables.

Implemented optional split-domain WebFinger support with HANDLE_HOST and WEB_ORIGIN variables.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: high

Feature Medium

Added REMOTE_MEDIA_THUMBNAILS env variable to control remote thumbnail generation.

Added REMOTE_MEDIA_THUMBNAILS env variable to control remote thumbnail generation.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: high

Feature Medium

Added public reaction list pages anchored to each local post.

Added public reaction list pages anchored to each local post.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: low

Feature Medium

Enabled per-account setting to make following list public via hide_collections flag.

Enabled per-account setting to make following list public via hide_collections flag.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: low

Feature Medium

Added ActivityPub quote-inline fallback for non-quote-aware clients.

Added ActivityPub quote-inline fallback for non-quote-aware clients.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: low

Feature Medium

Implemented media proxy with three levels: off, proxy, cache.

Implemented media proxy with three levels: off, proxy, cache.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: low

Feature Medium

Added LOG_FILE_FORMAT env variable for log file format selection (jsonl or logfmt).

Added LOG_FILE_FORMAT env variable for log file format selection (jsonl or logfmt).

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: low

Feature Medium

Enabled avatar and header image upload with drag-and-drop support in admin account forms.

Enabled avatar and header image upload with drag-and-drop support in admin account forms.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: low

Feature Medium

Refreshed entire server-rendered front-end with new design system, UnoCSS, and Lucide icons.

Refreshed entire server-rendered front-end with new design system, UnoCSS, and Lucide icons.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: low

Feature Medium

Upgraded Fedify to version 2.2.2.

Upgraded Fedify to version 2.2.2.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: low

Feature Medium

Added Traditional Chinese documentation.

Added Traditional Chinese documentation.

Source: granite4.1:8b-q6_K@2026-05-20

Confidence: low

Feature Low

TIMELINE_INBOXES feature flag now defaults to true; will be removed in Hollo 1.0.0 when timeline inbox mode becomes mandatory.

TIMELINE_INBOXES feature flag now defaults to true; will be removed in Hollo 1.0.0 when timeline inbox mode becomes mandatory.

Source: granite4.1:30b@2026-05-20-audit

Confidence: high

Feature Low

Added public Followers and Following pages with pagination (100 entries) and substring search; following list visibility controlled by hide_collections flag.

Added public Followers and Following pages with pagination (100 entries) and substring search; following list visibility controlled by hide_collections flag.

Source: granite4.1:30b@2026-05-20-audit

Confidence: low

Performance Low

Improved authenticated API request performance by replacing complex JOIN query in tokenRequired middleware with lightweight single-table lookup and on-demand account data fetching.

Improved authenticated API request performance by replacing complex JOIN query in tokenRequired middleware with lightweight single-table lookup and on-demand account data fetching.

Source: granite4.1:30b@2026-05-20-audit

Confidence: high

Performance Low

Optimized Mastodon-compatible timeline loading after Drizzle upgrade by reducing relation graph size, fetching post IDs first, and capping limit at 1000.

Optimized Mastodon-compatible timeline loading after Drizzle upgrade by reducing relation graph size, fetching post IDs first, and capping limit at 1000.

Source: granite4.1:30b@2026-05-20-audit

Confidence: high

Performance Low

Optimized NodeInfo endpoint by rewriting activeMonth/halfyear queries with EXISTS semi-join, adding composite B-tree index on (actor_id, updated), and introducing 5‑minute in‑process TTL cache.

Optimized NodeInfo endpoint by rewriting activeMonth/halfyear queries with EXISTS semi-join, adding composite B-tree index on (actor_id, updated), and introducing 5‑minute in‑process TTL cache.

Source: granite4.1:30b@2026-05-20-audit

Confidence: high

Performance Low

Optimized GET /api/v2/instance endpoint by adding partial composite index on (actor_id, language) where language IS NOT NULL and caching responses for 5 minutes.

Optimized GET /api/v2/instance endpoint by adding partial composite index on (actor_id, language) where language IS NOT NULL and caching responses for 5 minutes.

Source: granite4.1:30b@2026-05-20-audit

Confidence: high

Performance Low

Improved hashtag timeline and featured tag performance with GIN index on posts.tags column.

Improved hashtag timeline and featured tag performance with GIN index on posts.tags column.

Source: granite4.1:30b@2026-05-20-audit

Confidence: high

Bugfix High

Fixed performance bug causing profile page queries to take hundreds of seconds on cold PostgreSQL cache; added composite index (actor_id, published) and removed unnecessary lateral joins for replies in list contexts.

Fixed performance bug causing profile page queries to take hundreds of seconds on cold PostgreSQL cache; added composite index (actor_id, published) and removed unnecessary lateral joins for replies in list contexts.

Source: granite4.1:30b@2026-05-20-audit

Confidence: high

Bugfix Medium

Cancel running PostgreSQL queries when the corresponding GET or HEAD request is aborted, reducing wasted database work.

Cancel running PostgreSQL queries when the corresponding GET or HEAD request is aborted, reducing wasted database work.

Source: granite4.1:30b@2026-05-20-audit

Confidence: high

Bugfix Medium

Fixed credential update endpoint from silently wiping all custom profile fields when no fields_attributes were provided.

Fixed credential update endpoint from silently wiping all custom profile fields when no fields_attributes were provided.

Source: granite4.1:30b@2026-05-20-audit

Confidence: high

Bugfix Medium

Fixed crash when persisting ActivityPub posts with a published date before Unix epoch (caused uuidv7() negative timestamp).

Fixed crash when persisting ActivityPub posts with a published date before Unix epoch (caused uuidv7() negative timestamp).

Source: granite4.1:30b@2026-05-20-audit

Confidence: high

Bugfix Medium

Fixed account edit page causing unnecessary network lookup of @[email protected] regardless of news-following change; now only looks up when the setting actually changes.

Fixed account edit page causing unnecessary network lookup of @[email protected] regardless of news-following change; now only looks up when the setting actually changes.

Source: granite4.1:30b@2026-05-20-audit

Confidence: low

Bugfix Low

Fixed preview card generation for remote posts whose content only links to mentioned accounts.

Fixed preview card generation for remote posts whose content only links to mentioned accounts.

Source: granite4.1:30b@2026-05-20-audit

Confidence: high

Refactor Low

Upgraded Drizzle ORM to 1.0.0-rc.2 and migrated relational query definitions to new relations API.

Upgraded Drizzle ORM to 1.0.0-rc.2 and migrated relational query definitions to new relations API.

Source: granite4.1:30b@2026-05-20-audit

Confidence: high

Full changelog

Released on May 20, 2026.

  • Upgraded Drizzle ORM to 1.0.0-rc.2 and migrated Hollo's relational query definitions to the new relations API. This has no intended user-facing behavior changes, but the first migration after upgrading may need one extra database permission check. Drizzle 1 upgrades its own migration log table, drizzle.__drizzle_migrations, by adding name and applied_at columns. PostgreSQL only allows that ALTER TABLE when the migration is run by the table owner.

    If pnpm run migrate fails with must be owner of table __drizzle_migrations, first check which role owns Drizzle's migration log table:

    SELECT
      n.nspname AS schema,
      c.relname AS table_name,
      pg_get_userbyid(c.relowner) AS owner
    FROM pg_class c
    JOIN pg_namespace n ON n.oid = c.relnamespace
    WHERE n.nspname = 'drizzle'
      AND c.relname = '__drizzle_migrations';
    

    If the owner is not the same role Hollo uses in DATABASE_URL, run the following SQL as the current table owner or a database admin, replacing hollo with your Hollo database user:

    ALTER SCHEMA drizzle OWNER TO hollo;
    ALTER TABLE drizzle.__drizzle_migrations OWNER TO hollo;
    ALTER SEQUENCE IF EXISTS drizzle.__drizzle_migrations_id_seq
      OWNER TO hollo;
    

    Then run pnpm run migrate again with Hollo's normal DATABASE_URL. If your database provider does not allow ownership transfers, run this one migration once with the database role that already owns drizzle.__drizzle_migrations.

  • Cancel running PostgreSQL queries when the corresponding GET or HEAD request is aborted, such as when a client disconnects or quickly switches away from a timeline column. Hollo now tracks the underlying postgres.js query handles used by Drizzle and calls PostgreSQL cancellation for in-flight queries tied to the aborted read-only request. This reduces wasted database work and helps keep the connection pool available under repeated client-side aborts. The existing statement_timeout recommendation remains useful as a backstop for queries that cannot be tied to a safe HTTP request.

  • Added passkey (WebAuthn) authentication. The admin Auth page now has a “Passkeys” section for enrolling and managing passkeys, and the public login page presents a “Sign in with passkey” button (with the email/password form tucked behind a toggle) whenever at least one passkey is enrolled. Both device-bound and synced (multi-device) passkeys are accepted. A passkey on its own counts as multi-factor authentication, so a successful passkey sign-in is accepted in place of the TOTP step — the user is not asked for a one-time code in the same session.

    Hollo uses the @simplewebauthn/server library for verification and ships the matching browser helper as a static asset linked only from the auth and login pages. Registration uses residentKey: required and userVerification: required, so every enrolled passkey is discoverable and tied to a biometric or PIN gesture. Registration challenges are bound to the current login session with a server-enforced 5-minute TTL, and login challenges are stored in a single-use passkey_login_challenges table so a captured cookie + assertion pair can be redeemed at most once. [#487]

  • Added optional split-domain WebFinger support. When the new HANDLE_HOST and WEB_ORIGIN environment variables are set, Hollo uses Fedify's origin configuration so that fediverse handles (e.g. @[email protected]) and ActivityPub actor URIs (e.g. https://ap.example.com/@alice) can live on different domains. The Mastodon-compatible /api/v1/instance and /api/v2/instance endpoints expose HANDLE_HOST as the instance domain when this is configured, so clients display the correct @user@HANDLE_HOST handle. Both variables must be set together; setting only one is a startup error. They must be configured before the first account is created — changing the handle domain after federation has begun breaks remote follow relationships; on startup Hollo logs a warning when the configured HANDLE_HOST does not match the existing account's stored handle. Operators must also configure their reverse proxy on the handle domain to redirect /.well-known/webfinger to WEB_ORIGIN. See the Split-domain WebFinger guide. [#161, #484]

  • Added a media proxy that re-serves remote avatars, headers, post attachments, custom emojis, and preview-card images from Hollo's own origin. This sidesteps CORS configurations on remote object stores and prevents the visitor's browser from talking directly to the source server. Controlled by a new MEDIA_PROXY environment variable with three levels: [#481, #483, #493]

    • off (default): the Mastodon API and web UI hand the original remote URL to clients, matching the historical behaviour.
    • proxy: every remote media URL is rewritten to a signed /proxy/<sig>/<b64url> path served by Hollo itself. The proxy runs SSRF checks on the upstream URL and on every redirect target, allows only image/video/audio Content-Types (image/svg+xml is explicitly blocked to avoid same-origin XSS), caps the body at 32 MiB, and serves the response with Cache-Control: public, max-age=2592000, immutable and X-Content-Type-Options: nosniff. No on-disk cache.
    • cache: same URL rewriting, but the streamed body is persisted to the configured storage backend as proxy/<sha256>.bin, with a content-type sidecar alongside it at proxy/<sha256>.json. Subsequent requests skip the upstream fetch. Remote actor avatars for accounts with an approved follow relationship to the local account are also prefetched into this same cache when the actor is stored or refreshed, so stale upstream avatar files can keep rendering after Hollo has seen them once. The admin dashboard at /thumbnail_cleanup can purge the cache on demand.

    MEDIA_PROXY also accepts the Boolean synonyms true/on/1 (as aliases for proxy) and false/off/0 (as aliases for off). Disk caching is opt-in only via the explicit cache value.

    Outbound federation is unaffected: Hollo still publishes the original remote URLs in ActivityPub icon, image, attachment, and emoji Tag references.

  • Added a REMOTE_MEDIA_THUMBNAILS environment variable that controls whether Hollo downloads incoming remote attachments to generate a local WebP thumbnail. Set to off to skip the upstream fetch and Sharp pipeline entirely, storing the remote URL itself as the thumbnail URL—useful in combination with MEDIA_PROXY=proxy or cache to free up the disk space the local thumbnails would otherwise occupy. Defaults to on (the historical behavior). [#481, #483]

  • Added FEP-044f quote authorization and policy support on top of the Mastodon-compatible quote APIs. [#457, #459, #460]

    • Added persistent quote states for pending, accepted, rejected, revoked, and unauthorized quotes, plus quote target and authorization IRIs for federation.
    • Hollo now enforces quote policy, quote target visibility, block relationships, follower-only quote permissions, and direct-message mention requirements when creating a quote through POST /api/v1/statuses. Remote public posts without an advertised FEP-044f quote policy are treated as legacy quote targets and can be quoted without waiting for quote authorization.
    • Implemented quote_approval_policy handling on status creation and editing, and added PUT /api/v1/statuses/:id/interaction_policy for updating a status' quote policy after publication.
    • quotes_count now includes only accepted quotes and is updated when quotes are accepted, rejected, revoked, created, deleted, or received through federation.
    • GET /api/v1/statuses/:id/quotes now lists only accepted quotes, and quote revocation keeps quote target metadata while removing the quote from accepted quote lists and counts.
    • Published outbound FEP-044f quote, quoteAuthorization, and interactionPolicy.canQuote properties on ActivityPub objects, while keeping the legacy quoteUrl property for compatibility.
    • Parsed inbound FEP-044f quote targets and quote approval policies from remote objects, including support for the legacy quoteUrl property.
    • Added federation handling for QuoteRequest, Accept(QuoteRequest), Reject(QuoteRequest), and Delete(QuoteAuthorization), allowing Hollo to request quote authorization from remote servers, accept or reject incoming quote requests, and revoke quotes when a remote quote authorization is deleted.
    • Added dereferenceable local QuoteAuthorization ActivityPub objects for accepted quotes.
  • Added custom field editing to the admin account creation and editing forms, allowing up to 10 label–value pairs per profile (beyond Mastodon's limit of 4). Field values support Markdown and mention syntax. The Mastodon-compatible PATCH /api/v1/accounts/ update_credentials endpoint now also accepts up to 10 custom fields via fields_attributes[0] through fields_attributes[9].

  • Added LOG_FILE_FORMAT environment variable to control the format of the log file set by LOG_FILE. Valid values are jsonl (the default, JSON Lines format) and logfmt (logfmt format).

  • Fixed preview card generation for remote posts whose content only links to mentioned accounts. Hollo now excludes ActivityPub Mention targets from remote preview-card link detection even when the source server, such as NodeBB, emits plain profile links in the post HTML instead of links marked with a mention class.

  • Fixed a bug in PATCH /api/v1/accounts/update_credentials where submitting any credential update (e.g. display_name) without fields_attributes would silently wipe all existing custom profile fields from the public profile, API responses, and federation output.

  • Added an ActivityPub quote-inline fallback to the content of explicit quote posts created through the Mastodon API. Software that does not support quote posts can now still show the quoted post permalink, while quote-aware software can hide the fallback paragraph by class name.

  • Hid incoming quote-inline fallback paragraphs from Mastodon API status content and profile pages when Hollo can render the structured quoted post. If the quoted post is unavailable, the fallback link remains visible so the quoted URL is not lost.

  • Posts on the public profile and hashtag pages now render Open Graph link previews—a thumbnail (when og:image is set), the host name, the page title, and a short description—for each post that has a stored preview_card. Posts with attached media or shown as quoted posts hide the preview to avoid visual clutter. [#458]

  • Refreshed the entire server-rendered front-end. Hollo replaces Pico CSS with a new design system documented in DESIGN.md and styled through UnoCSS (Wind4, Icons, Typography, and Web Fonts presets). [#458]

    • The design language is achromatic by default; each account owner's theme color tints the profile, post, hashtag, and owner-specific dashboard pages through --theme-50 through --theme-950 CSS variables injected on <html>.
    • Web Fonts are loaded from bunny.net: Inter for Latin, Noto Sans KR/JP/SC for CJK, and JetBrains Mono for code. Lucide icons replace ad-hoc iconography.
    • Every public and dashboard page was rebuilt: login, setup, OTP, the public home, account profiles, single post permalinks, the hashtag stream, the account list and editor, custom emojis, federation, thumbnail cleanup, the OAuth consent screen, and the dashboard auth (2FA) panel.
    • Posts render their bodies as prose markdown with brand-colored links, attached media as a two-column grid, polls as brand-tinted bars, quoted posts as an inset card, and link previews as a media-object card. The single-post permalink page additionally enlarges the focal post.
    • Forms share a small set of primitives (Field, TextField, TextareaField, SelectField, CheckboxField, FieldSection, and SubmitButton) that the auth, account, emoji, federation, thumbnail cleanup, and migrate forms all use. The theme color picker on the account form is a 20-swatch grid; the username field is bracketed by @ and @host chips so the resulting fediverse handle is obvious.
    • Dark mode follows prefers-color-scheme: dark automatically, with primary buttons stepping from brand-600 to brand-700 so they don't dominate dark surfaces. Text selection adopts the active brand color.
    • Hollo's logos are self-hosted from /public/ instead of being fetched from jsDelivr. The 22 Pico-generated *.min.css files are removed; UnoCSS emits a single src/public/uno.css whose URL is cache-busted by file mtime.
  • Added public Followers and Following pages on profile screens, with pagination (100 entries per page) and a substring search filter over display name and handle. The followers list is always public. The following list is gated by a new per-account setting, Make following list public (off by default); when off, the page returns 404 Not Found, the corresponding ActivityPub following collection is hidden from federation, and the Mastodon-compatible GET /api/v1/accounts/:id/following endpoint returns an empty array. The toggle also round-trips through PATCH update_credentials as hide_collections. [#491]

  • Added public reaction list pages anchored to each local post: /@:handle/:id/likes lists the accounts that liked the post, /@:handle/:id/shares lists the accounts that boosted it, /@:handle/:id/reactions/:emoji lists the accounts that reacted with a specific emoji, and /@:handle/:id/quotes lists the posts that quote it. Each page features the original post above the list. On the profile feed and post permalink page, the per-post like, share, quote, and reaction-emoji indicators now link into these pages for local posts; remote posts continue to display the counts as plain text. [#490]

  • Added avatar and header image upload to the admin account creation and editing forms, with drag-and-drop support and in-page image preview. Files are stored using the same storage backend as the Mastodon-compatible API (PATCH /api/v1/accounts/update_credentials).

  • Search queries on account handles, names, and post content now use PostgreSQL trigram indexes via the pg_trgm extension, improving ILIKE search performance. The pg_trgm extension is enabled automatically when the migration is applied.

  • Fixed a performance bug on the account edit page where saving an account always triggered a network lookup of @[email protected] regardless of whether the “Receive Hollo news” setting had actually changed. The lookup now only happens when the news-following state genuinely changes.

  • Improved the performance of authenticated API requests by replacing the complex multi-table JOIN query in the tokenRequired middleware with a lightweight single-table lookup. Account owner data is now fetched on demand only by routes that actually need it, and requests that fail scope validation no longer touch the accounts table at all. [#127, #467]

  • Fixed a bug where incoming ActivityPub posts with timestamps more than 12 hours in the future were accepted and stuck to the top of the federated timeline, enabling timeline manipulation via forged timestamps. Such posts are now silently ignored. [#67, #466]

  • Fixed a crash when persisting ActivityPub posts with a published date before the Unix epoch (January 1, 1970), which caused uuidv7() to receive a negative timestamp. [#67, #466]

  • Fixed min_id handling on GET /api/v1/timelines/public, GET /api/v1/timelines/home, GET /api/v1/timelines/list/:list_id, and GET /api/v1/timelines/tag/:hashtag to follow Mastodon's pagination semantics: min_id now returns the posts immediately newer than the cursor (rather than the most recent posts above it), so gap-loading clients such as SubwayTooter can converge on arbitrarily large gaps. since_id is now honoured on these endpoints as well, and min_id takes precedence when both are supplied. Timeline responses also include a rel="prev" entry in the Link header alongside the existing rel="next" entry, and keep rel="next" even when a bounded gap page returns fewer statuses than the requested limit, so clients no longer have to guess which cursor parameter to use or stop walking partial gaps. [#479, #482, #492]

  • Optimized Mastodon-compatible timeline loading after the Drizzle ORM upgrade. Home, list, public, and hashtag timelines now use a smaller relation graph that avoids fetching columns and nested relations that are not needed for timeline status serialization. They also fetch matching post IDs first, then hydrate only those posts with the timeline status projection, so empty cursor checks and heavily filtered timelines avoid running the relation-heavy query at all. The ID selection and hydration run in the same read-only repeatable-read snapshot, preserving timeline eligibility consistency while reducing the size and cost of the SQL generated for large timelines, especially list timelines on instances with many stored posts, without changing the API response format. Timeline limit query parameters are now capped at 1000 to keep these queries bounded even when clients request unusually large pages.

  • Fixed a performance bug that caused profile page queries to take hundreds of seconds on a cold PostgreSQL buffer cache and trigger a thundering herd that exhausted the connection pool. Two root causes: [#489]

    • posts.actor_id had no composite index with published, so timeline queries that ORDER BY published DESC performed a full sort of all matching rows. A B-tree index on (actor_id, published) is now created by migration.
    • Every API response that fetches a list of posts used a lateral join to eagerly load all direct replies for each post, even though only the denormalized replies_count counter is actually used in the Mastodon API serialization. The lateral join is removed from all list-context queries (getPostRelations, timelines, notifications, outbox, etc.). The ActivityPub object serializer now also uses replies_count for the replies collection totalItems instead of reloading every reply. Single-post views (the public post permalink page) still fetch replies but are now capped at 20 per request.
  • Fixed a performance bug where the NodeInfo endpoint performed a sequential scan of the entire posts table on every request because posts.updated had no index. On large instances (e.g. 4 M rows / 13 GB) this took 15–20 seconds per query; since NodeInfo is polled frequently (Caddy health checks, remote servers), the slow queries piled up and could exhaust the connection pool. A B-tree index on posts.updated is now created by migration. [#488]

  • Further optimized the NodeInfo endpoint with three changes that together reduce a previously 42-second query to a handful of milliseconds:

    • Rewrote the activeMonth and activeHalfyear queries from a RIGHT JOIN + countDistinct (full scan of all posts) to an EXISTS semi-join that performs at most one index lookup per local account owner.
    • Added a composite B-tree index on (actor_id, updated) to support the EXISTS subquery without heap fetches.
    • Added a 5-minute in-process TTL cache on the NodeInfo dispatcher so repeated polls by external directories and health checkers no longer hit the database at all.
  • Optimized the GET /api/v2/instance endpoint. The languages field was previously computed by a full scan of the posts table on every request. A new partial composite index posts_actor_id_language_index on (actor_id, language) WHERE language IS NOT NULL turns this into an Index-Only Scan, and a 5-minute in-process response cache absorbs repeated calls so the database is queried at most once per 5 minutes.

  • Improved hashtag timeline and featured tag performance by adding a GIN index on posts.tags. All ? and ?| operator lookups on the tags column (hashtag timeline, featured tag counts, profile hashtag filters, and tag pages) previously caused a full sequential scan of the 4 M+ row posts table, taking several seconds even on a warm cache. The new index cuts these to index lookups.

  • The TIMELINE_INBOXES feature flag, introduced in Hollo 0.4.0, now defaults to true. Set it to false explicitly to opt out. The flag will be removed entirely in Hollo 1.0.0, when timeline inbox mode will be the only supported behavior.

  • Upgraded Fedify to 2.2.2.

  • Added Traditional Chinese (繁體中文; zh-TW) documentation.

Breaking Changes

  • Drizzle ORM upgraded to 1.0.0‑rc.2; migration adds `name` and `applied_at` columns to `drizzle.__drizzle_migrations`, requiring table ownership for the ALTER TABLE operation.

Weekly OSS security release digest.

The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.

No spam, unsubscribe anytime.

Share this release

Track hollo

Get notified when new releases ship.

Sign up free

About hollo

Federated single-user microblogging software

All releases →

Related context

Beta — feedback welcome: [email protected]