Skip to content

Migrating from v1

v2 consolidates HappyView, Tap, and AIP into a single binary. Real-time indexing, backfill, and OAuth are now built in — there are no companion services to deploy.

This guide covers every breaking change and the steps to migrate.

v1v2
HappyView + Tap + AIP (3 services)Single HappyView binary
Postgres onlySQLite (default) or Postgres
AIP handles OAuthBuilt-in atproto OAuth with DPoP
Tap handles indexing + backfillBuilt-in Jetstream streaming + backfill
Offset-based paginationCursor-based pagination
Admin bootstrapping via configFirst authenticated user becomes admin

Tap and AIP are no longer needed. Remove them from your docker-compose.yml (or equivalent) and delete any associated containers/volumes.

Environment variables to remove:

VariableReason
AIP_URLOAuth is now built in
AIP_PUBLIC_URLNo longer needed
TAP_URLIndexing is now built in
TAP_ADMIN_PASSWORDNo longer needed
TAP_DATABASE_URLTap no longer exists
TAP_RELAY_URLUse RELAY_URL on HappyView directly
TAP_PLC_URLUse PLC_URL on HappyView directly
TAP_COLLECTION_FILTERSCollection filtering now uses lexicon config
TAP_SIGNAL_COLLECTIONSCollection filtering now uses lexicon config

New required variables:

VariableDescription
PUBLIC_URLPublic-facing URL (e.g. https://happyview.example.com). Used for OAuth callbacks
SESSION_SECRETSecret key for signing session cookies (at least 64 chars). Must be set in production

New optional variables:

VariableDefaultDescription
DATABASE_BACKENDauto-detectedForce sqlite or postgres
JETSTREAM_URLwss://jetstream1.us-east.bsky.networkReplaces Tap’s Jetstream connection
STATIC_DIR./web/outDashboard static assets directory
TOKEN_ENCRYPTION_KEY---Base64-encoded 32-byte key for encrypting stored OAuth tokens. Strongly recommended in production
DEFAULT_RATE_LIMIT_CAPACITY100Default token bucket capacity for new API clients
DEFAULT_RATE_LIMIT_REFILL_RATE2.0Default refill rate (tokens/sec) for new API clients
APP_NAME---App name shown on OAuth screens
LOGO_URI---Logo URL for OAuth screens
TOS_URI---Terms of service URL
POLICY_URI---Privacy policy URL

Unchanged variables: DATABASE_URL, HOST, PORT, RELAY_URL, PLC_URL, EVENT_LOG_RETENTION_DAYS, RUST_LOG.

See Configuration for the full reference.

v2 defaults to SQLite. If you’re running Postgres in v1, you have two options:

Keep Postgres — no changes needed. Your DATABASE_URL stays the same and v2 auto-detects the backend from the connection string.

Migrate to SQLite — follow the Postgres to SQLite migration guide. SQLite is simpler to operate (no separate database server) and is the recommended default for most deployments.

db.query no longer supports offset. Replace offset-based pagination with cursors:

Before (v1):

local result = db.query({
collection = collection,
limit = limit,
offset = page * limit,
})

After (v2):

local result = db.query({
collection = collection,
limit = limit,
cursor = params.cursor,
})
-- result.cursor is an opaque string; pass it back as ?cursor= for the next page

Clients should pass the cursor value from the response as a query parameter to fetch the next page. Don’t parse or construct cursors — they’re opaque.

v2 adds several new Lua APIs that you can optionally adopt:

v1 API keys used the hv_ prefix. v2 keeps existing hv_ keys working but new keys use the hv_ prefix as well. No migration needed.

v2 also adds API clients for third-party OAuth apps, which use the hvc_ prefix. These are separate from API keys — see the API Clients guide.

The dashboard has moved from the root path to /dashboard:

v1v2
//dashboard
/lexicons/dashboard/lexicons
/records/dashboard/records
/settings/dashboard/settings

Update any bookmarks or internal links.

v2 introduces granular user permissions. After upgrading:

  1. The first user to authenticate becomes the super user (full access).
  2. Additional users are created with no permissions by default.
  3. Assign permissions or use a template (Viewer, Operator, Manager, Full Access).

See the Permissions guide for details.

Before (v1):

services:
postgres:
image: postgres:17
# ...
tap:
image: ghcr.io/bluesky-social/indigo/tap:latest
environment:
TAP_DATABASE_URL: postgres://...
TAP_RELAY_URL: https://bsky.network
TAP_ADMIN_PASSWORD: secret
depends_on: [postgres]
happyview:
image: ghcr.io/gamesgamesgamesgamesgames/happyview:latest
environment:
DATABASE_URL: postgres://...
AIP_URL: http://aip:8080
TAP_URL: http://tap:2480
TAP_ADMIN_PASSWORD: secret
depends_on: [postgres, tap]

After (v2):

services:
happyview:
image: ghcr.io/gamesgamesgamesgamesgames/happyview:latest
environment:
DATABASE_URL: sqlite://data/happyview.db?mode=rwc
PUBLIC_URL: https://happyview.example.com
SESSION_SECRET: your-64-char-secret
volumes:
- data:/app/data
volumes:
data:

Or with Postgres:

services:
postgres:
image: postgres:17
# ...
happyview:
image: ghcr.io/gamesgamesgamesgamesgames/happyview:latest
environment:
DATABASE_URL: postgres://happyview:happyview@postgres/happyview
PUBLIC_URL: https://happyview.example.com
SESSION_SECRET: your-64-char-secret
depends_on: [postgres]
  • Remove Tap and AIP services
  • Remove old environment variables (AIP_URL, TAP_URL, TAP_ADMIN_PASSWORD, etc.)
  • Add PUBLIC_URL and SESSION_SECRET
  • Add TOKEN_ENCRYPTION_KEY (recommended for production)
  • Decide on SQLite (default) or keep Postgres
  • Update Lua scripts to use cursor-based pagination instead of offsets
  • Update any bookmarks/links to use /dashboard prefix
  • Set up user permissions after first login