···11+# Migrating from v1
22+33+v2 consolidates HappyView, [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap), and [AIP](https://github.com/graze-social/aip) into a single binary. Real-time indexing, backfill, and OAuth are now built in — there are no companion services to deploy.
44+55+This guide covers every breaking change and the steps to migrate.
66+77+## Architecture changes
88+99+| v1 | v2 |
1010+|----|-----|
1111+| HappyView + Tap + AIP (3 services) | Single HappyView binary |
1212+| Postgres only | SQLite (default) or Postgres |
1313+| AIP handles OAuth | Built-in atproto OAuth with DPoP |
1414+| Tap handles indexing + backfill | Built-in Jetstream streaming + backfill |
1515+| Offset-based pagination | Cursor-based pagination |
1616+| Admin bootstrapping via config | First authenticated user becomes admin |
1717+1818+## 1. Remove Tap and AIP
1919+2020+Tap and AIP are no longer needed. Remove them from your `docker-compose.yml` (or equivalent) and delete any associated containers/volumes.
2121+2222+**Environment variables to remove:**
2323+2424+| Variable | Reason |
2525+|----------|--------|
2626+| `AIP_URL` | OAuth is now built in |
2727+| `AIP_PUBLIC_URL` | No longer needed |
2828+| `TAP_URL` | Indexing is now built in |
2929+| `TAP_ADMIN_PASSWORD` | No longer needed |
3030+| `TAP_DATABASE_URL` | Tap no longer exists |
3131+| `TAP_RELAY_URL` | Use `RELAY_URL` on HappyView directly |
3232+| `TAP_PLC_URL` | Use `PLC_URL` on HappyView directly |
3333+| `TAP_COLLECTION_FILTERS` | Collection filtering now uses lexicon config |
3434+| `TAP_SIGNAL_COLLECTIONS` | Collection filtering now uses lexicon config |
3535+3636+## 2. Update environment variables
3737+3838+**New required variables:**
3939+4040+| Variable | Description |
4141+|----------|-------------|
4242+| `PUBLIC_URL` | Public-facing URL (e.g. `https://happyview.example.com`). Used for OAuth callbacks |
4343+| `SESSION_SECRET` | Secret key for signing session cookies (at least 64 chars). **Must be set in production** |
4444+4545+**New optional variables:**
4646+4747+| Variable | Default | Description |
4848+|----------|---------|-------------|
4949+| `DATABASE_BACKEND` | auto-detected | Force `sqlite` or `postgres` |
5050+| `JETSTREAM_URL` | `wss://jetstream1.us-east.bsky.network` | Replaces Tap's Jetstream connection |
5151+| `STATIC_DIR` | `./web/out` | Dashboard static assets directory |
5252+| `TOKEN_ENCRYPTION_KEY` | --- | Base64-encoded 32-byte key for encrypting stored OAuth tokens. **Strongly recommended in production** |
5353+| `DEFAULT_RATE_LIMIT_CAPACITY` | `100` | Default token bucket capacity for new API clients |
5454+| `DEFAULT_RATE_LIMIT_REFILL_RATE` | `2.0` | Default refill rate (tokens/sec) for new API clients |
5555+| `APP_NAME` | --- | App name shown on OAuth screens |
5656+| `LOGO_URI` | --- | Logo URL for OAuth screens |
5757+| `TOS_URI` | --- | Terms of service URL |
5858+| `POLICY_URI` | --- | Privacy policy URL |
5959+6060+**Unchanged variables:** `DATABASE_URL`, `HOST`, `PORT`, `RELAY_URL`, `PLC_URL`, `EVENT_LOG_RETENTION_DAYS`, `RUST_LOG`.
6161+6262+See [Configuration](../getting-started/configuration.md) for the full reference.
6363+6464+## 3. Choose your database
6565+6666+v2 defaults to SQLite. If you're running Postgres in v1, you have two options:
6767+6868+**Keep Postgres** — no changes needed. Your `DATABASE_URL` stays the same and v2 auto-detects the backend from the connection string.
6969+7070+**Migrate to SQLite** — follow the [Postgres to SQLite migration guide](database/postgres-to-sqlite-migration.md). SQLite is simpler to operate (no separate database server) and is the recommended default for most deployments.
7171+7272+## 4. Update Lua scripts
7373+7474+### Cursor-based pagination (breaking change)
7575+7676+`db.query` no longer supports `offset`. Replace offset-based pagination with cursors:
7777+7878+**Before (v1):**
7979+8080+```lua
8181+local result = db.query({
8282+ collection = collection,
8383+ limit = limit,
8484+ offset = page * limit,
8585+})
8686+```
8787+8888+**After (v2):**
8989+9090+```lua
9191+local result = db.query({
9292+ collection = collection,
9393+ limit = limit,
9494+ cursor = params.cursor,
9595+})
9696+9797+-- result.cursor is an opaque string; pass it back as ?cursor= for the next page
9898+```
9999+100100+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.
101101+102102+### New APIs available
103103+104104+v2 adds several new Lua APIs that you can optionally adopt:
105105+106106+- [`atproto.resolve_service_endpoint`](../reference/lua/atproto-api.md) — resolve a DID to its PDS endpoint
107107+- [`atproto.get_labels`](../reference/lua/atproto-api.md) / [`atproto.get_labels_batch`](../reference/lua/atproto-api.md) — fetch content labels from subscribed labelers
108108+- [`os.time`](../reference/lua/standard-libraries.md), `os.date`, `os.difftime`, `os.clock` — safe `os` subset
109109+110110+## 5. Update API key prefixes
111111+112112+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.
113113+114114+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](features/api-clients.md).
115115+116116+## 6. Update the dashboard URL
117117+118118+The dashboard has moved from the root path to `/dashboard`:
119119+120120+| v1 | v2 |
121121+|----|-----|
122122+| `/` | `/dashboard` |
123123+| `/lexicons` | `/dashboard/lexicons` |
124124+| `/records` | `/dashboard/records` |
125125+| `/settings` | `/dashboard/settings` |
126126+127127+Update any bookmarks or internal links.
128128+129129+## 7. User permissions
130130+131131+v2 introduces granular user permissions. After upgrading:
132132+133133+1. The first user to authenticate becomes the **super user** (full access).
134134+2. Additional users are created with no permissions by default.
135135+3. Assign permissions or use a template (Viewer, Operator, Manager, Full Access).
136136+137137+See the [Permissions guide](admin/permissions.md) for details.
138138+139139+## 8. Docker Compose (example)
140140+141141+**Before (v1):**
142142+143143+```yaml
144144+services:
145145+ postgres:
146146+ image: postgres:17
147147+ # ...
148148+149149+ tap:
150150+ image: ghcr.io/bluesky-social/indigo/tap:latest
151151+ environment:
152152+ TAP_DATABASE_URL: postgres://...
153153+ TAP_RELAY_URL: https://bsky.network
154154+ TAP_ADMIN_PASSWORD: secret
155155+ depends_on: [postgres]
156156+157157+ happyview:
158158+ image: ghcr.io/gamesgamesgamesgamesgames/happyview:latest
159159+ environment:
160160+ DATABASE_URL: postgres://...
161161+ AIP_URL: http://aip:8080
162162+ TAP_URL: http://tap:2480
163163+ TAP_ADMIN_PASSWORD: secret
164164+ depends_on: [postgres, tap]
165165+```
166166+167167+**After (v2):**
168168+169169+```yaml
170170+services:
171171+ happyview:
172172+ image: ghcr.io/gamesgamesgamesgamesgames/happyview:latest
173173+ environment:
174174+ DATABASE_URL: sqlite://data/happyview.db?mode=rwc
175175+ PUBLIC_URL: https://happyview.example.com
176176+ SESSION_SECRET: your-64-char-secret
177177+ volumes:
178178+ - data:/app/data
179179+180180+volumes:
181181+ data:
182182+```
183183+184184+Or with Postgres:
185185+186186+```yaml
187187+services:
188188+ postgres:
189189+ image: postgres:17
190190+ # ...
191191+192192+ happyview:
193193+ image: ghcr.io/gamesgamesgamesgamesgames/happyview:latest
194194+ environment:
195195+ DATABASE_URL: postgres://happyview:happyview@postgres/happyview
196196+ PUBLIC_URL: https://happyview.example.com
197197+ SESSION_SECRET: your-64-char-secret
198198+ depends_on: [postgres]
199199+```
200200+201201+## Checklist
202202+203203+- [ ] Remove Tap and AIP services
204204+- [ ] Remove old environment variables (`AIP_URL`, `TAP_URL`, `TAP_ADMIN_PASSWORD`, etc.)
205205+- [ ] Add `PUBLIC_URL` and `SESSION_SECRET`
206206+- [ ] Add `TOKEN_ENCRYPTION_KEY` (recommended for production)
207207+- [ ] Decide on SQLite (default) or keep Postgres
208208+- [ ] Update Lua scripts to use cursor-based pagination instead of offsets
209209+- [ ] Update any bookmarks/links to use `/dashboard` prefix
210210+- [ ] Set up user permissions after first login