a love letter to tangled (android, iOS, and a search API)
19
fork

Configure Feed

Select the types of activity you want to include in your feed.

build: add migration service

+251 -122
+13 -3
README.md
··· 42 42 just db-up 43 43 ``` 44 44 45 + Bootstrap the schema once: 46 + 47 + ```bash 48 + just api-build 49 + DATABASE_URL="postgresql://localhost/${USER}_dev?sslmode=disable" \ 50 + ./packages/api/twister migrate 51 + ``` 52 + 45 53 Run the mobile app: 46 54 47 55 ```bash ··· 79 87 80 88 ## Deployment 81 89 82 - Production deployment now uses Coolify plus a separate Coolify-managed 83 - PostgreSQL instance. The backend services are defined in 84 - `docker-compose.prod.yaml`. 90 + `docker-compose.prod.yaml` is now the source-of-truth VPS stack. It runs 91 + `postgres`, `migrate`, `api`, `indexer`, `tap`, and `llama-embeddings`. 92 + 93 + The llama.cpp service is only deployment groundwork for a later embedding 94 + adapter. Search remains keyword-only for now. 85 95 86 96 See [`docs/reference/deployment-walkthrough.md`](docs/reference/deployment-walkthrough.md) 87 97 for the full setup, bootstrap, backup, and cutover flow.
+72 -2
docker-compose.prod.yaml
··· 1 1 services: 2 + postgres: 3 + image: pgvector/pgvector:pg17 4 + restart: unless-stopped 5 + environment: 6 + POSTGRES_USER: ${POSTGRES_USER:-twisted} 7 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-twisted} 8 + POSTGRES_DB: ${POSTGRES_DB:-twisted} 9 + volumes: 10 + - twisted-postgres:/var/lib/postgresql/data 11 + healthcheck: 12 + test: 13 + [ 14 + "CMD-SHELL", 15 + "pg_isready -h localhost -p 5432 -U ${POSTGRES_USER:-twisted} -d ${POSTGRES_DB:-twisted}", 16 + ] 17 + interval: 10s 18 + timeout: 5s 19 + retries: 5 20 + expose: 21 + - "5432" 22 + 23 + migrate: 24 + build: 25 + context: ./packages/api 26 + dockerfile: Dockerfile 27 + command: ["twister", "migrate"] 28 + restart: "no" 29 + environment: 30 + DATABASE_URL: ${DATABASE_URL:-postgresql://${POSTGRES_USER:-twisted}:${POSTGRES_PASSWORD:-twisted}@postgres:5432/${POSTGRES_DB:-twisted}?sslmode=disable} 31 + LOG_LEVEL: ${LOG_LEVEL:-info} 32 + LOG_FORMAT: ${LOG_FORMAT:-json} 33 + depends_on: 34 + postgres: 35 + condition: service_healthy 36 + 2 37 api: 3 38 build: 4 39 context: ./packages/api ··· 6 41 command: ["twister", "api"] 7 42 restart: unless-stopped 8 43 environment: 9 - DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required} 44 + DATABASE_URL: ${DATABASE_URL:-postgresql://${POSTGRES_USER:-twisted}:${POSTGRES_PASSWORD:-twisted}@postgres:5432/${POSTGRES_DB:-twisted}?sslmode=disable} 10 45 HTTP_BIND_ADDR: ${HTTP_BIND_ADDR:-:8080} 11 46 LOG_LEVEL: ${LOG_LEVEL:-info} 12 47 LOG_FORMAT: ${LOG_FORMAT:-json} ··· 21 56 ADMIN_AUTH_TOKEN: ${ADMIN_AUTH_TOKEN:-} 22 57 OAUTH_CLIENT_ID: ${OAUTH_CLIENT_ID:-} 23 58 OAUTH_REDIRECT_URIS: ${OAUTH_REDIRECT_URIS:-} 59 + depends_on: 60 + migrate: 61 + condition: service_completed_successfully 24 62 expose: 25 63 - "8080" 26 64 ··· 31 69 command: ["twister", "indexer"] 32 70 restart: unless-stopped 33 71 environment: 34 - DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required} 72 + DATABASE_URL: ${DATABASE_URL:-postgresql://${POSTGRES_USER:-twisted}:${POSTGRES_PASSWORD:-twisted}@postgres:5432/${POSTGRES_DB:-twisted}?sslmode=disable} 35 73 INDEXER_HEALTH_ADDR: ${INDEXER_HEALTH_ADDR:-:9090} 36 74 LOG_LEVEL: ${LOG_LEVEL:-info} 37 75 LOG_FORMAT: ${LOG_FORMAT:-json} ··· 39 77 TAP_AUTH_PASSWORD: ${TAP_AUTH_PASSWORD:?TAP_AUTH_PASSWORD is required} 40 78 INDEXED_COLLECTIONS: ${INDEXED_COLLECTIONS:?INDEXED_COLLECTIONS is required} 41 79 ENABLE_INGEST_ENRICHMENT: ${ENABLE_INGEST_ENRICHMENT:-true} 80 + depends_on: 81 + migrate: 82 + condition: service_completed_successfully 83 + tap: 84 + condition: service_started 42 85 expose: 43 86 - "9090" 44 87 ··· 56 99 expose: 57 100 - "2480" 58 101 102 + llama-embeddings: 103 + image: ghcr.io/ggml-org/llama.cpp:server 104 + restart: unless-stopped 105 + environment: 106 + HF_HOME: /models/hf-cache 107 + LLAMA_CACHE: /models 108 + command: 109 + - --hf-repo 110 + - ${LLAMA_MODEL_REPO:-nomic-ai/nomic-embed-text-v1.5-GGUF} 111 + - --hf-file 112 + - ${LLAMA_MODEL_FILE:-nomic-embed-text-v1.5.Q8_0.gguf} 113 + - --embeddings 114 + - --pooling 115 + - mean 116 + - --ctx-size 117 + - ${LLAMA_CONTEXT_SIZE:-2048} 118 + - --host 119 + - 0.0.0.0 120 + - --port 121 + - "8080" 122 + volumes: 123 + - twisted-llama:/models 124 + expose: 125 + - "8080" 126 + 59 127 volumes: 128 + twisted-postgres: 60 129 tap-data: 130 + twisted-llama:
+8 -6
docs/adr/storage.md
··· 7 7 ## Decision 8 8 9 9 Twisted will use PostgreSQL as the primary database backend for search, 10 - indexing, queue state, and activity cache. The production deploy target is a 11 - Coolify application for `api`, `indexer`, and `tap` plus a separate 12 - Coolify-managed PostgreSQL instance. 10 + indexing, queue state, and activity cache. The production deploy target is one 11 + Compose stack for `postgres`, `migrate`, `api`, `indexer`, `tap`, and 12 + `llama-embeddings`. Coolify can host that stack, but the Compose file remains 13 + the source of truth. 13 14 14 15 ## Why 15 16 16 17 - PostgreSQL is the better fit for long-running multi-service deployment. 17 - - Coolify gives the project a straightforward Git-to-deploy path with built-in 18 - Traefik and a managed database resource. 18 + - Coolify still gives the project a straightforward Git-to-deploy path with 19 + built-in Traefik when we want it. 19 20 - The current service shape already wants two long-lived processes writing to 20 21 one shared database. 21 22 - A local PostgreSQL workflow keeps development closer to production than the ··· 28 29 - one mainstream database for local and remote environments 29 30 - simpler production backups and restore story 30 31 - easier operational model for `api` and `indexer` 32 + - explicit migration ownership through a one-shot `migrate` command 31 33 - no dependency on Turso-specific SQLite extension behavior 32 34 33 35 ### Negative ··· 53 55 1. add PostgreSQL connection/config support and local defaults 54 56 2. add a primary PostgreSQL migration set 55 57 3. move search and store implementations to PostgreSQL 56 - 4. deploy `api`, `indexer`, and `tap` from `docker-compose.prod.yaml` 58 + 4. deploy the Compose stack from `docker-compose.prod.yaml` 57 59 5. rebuild data through `backfill`, `enrich`, and `reindex` 58 60 6. cut traffic over only after smoke checks pass 59 61
+12 -4
docs/reference/api.md
··· 13 13 | --- | --- | 14 14 | `api` | HTTP API server | 15 15 | `indexer` | Tap consumer and index writer | 16 + | `migrate` | apply embedded SQL migrations | 16 17 | `backfill` | register repos with Tap | 17 18 | `enrich` | fill missing repo names, handles, and web URLs | 18 19 | `reindex` | re-upsert documents and finalize the search index | ··· 57 58 `documents` stores a generated weighted `tsvector` column plus a GIN index for 58 59 keyword search. 59 60 61 + No embedding tables are active yet. `llama-embeddings` is deployed only as 62 + infra groundwork for a later semantic-search milestone. 63 + 60 64 ## Configuration 61 65 62 66 Primary env vars: ··· 88 92 89 93 ```sh 90 94 just db-up 95 + just api-build 96 + DATABASE_URL="postgresql://localhost/${USER}_dev?sslmode=disable" \ 97 + ./packages/api/twister migrate 91 98 just api-dev 92 99 just api-run-indexer 93 100 ``` 94 101 95 102 That dev compose file also runs Tap locally at `ws://localhost:2480/channel`. 103 + `api` and `indexer` no longer auto-apply schema changes on startup. 96 104 97 105 Use `just api-dev sqlite` only when you need the temporary SQLite rollback path. 98 106 ··· 100 108 101 109 Production uses: 102 110 103 - - Coolify Application with `docker-compose.prod.yaml` 104 - - separate Coolify-managed PostgreSQL resource 105 - - private Tap service from the pinned Indigo image 106 - - built-in Coolify Traefik for the public `api` domain 111 + - `docker-compose.prod.yaml` as the VPS stack source of truth 112 + - in-stack PostgreSQL on `pgvector/pgvector:pg17` 113 + - a one-shot `migrate` service before long-lived services start 114 + - private Tap and llama.cpp embedding services on the internal Compose network 107 115 108 116 See `docs/reference/deployment-walkthrough.md` for the full production flow.
+70 -80
docs/reference/deployment-walkthrough.md
··· 1 1 # Deployment Walkthrough 2 2 3 - Twisted deploys to Coolify as one Compose application with three services: 3 + Twisted now deploys as one VPS Compose stack defined by 4 + `docker-compose.prod.yaml`. Coolify can still host that stack, but the Compose 5 + file is the source of truth. 4 6 7 + ## Services 8 + 9 + - `postgres`: primary database on `pgvector/pgvector:pg17` 10 + - `migrate`: one-shot schema bootstrap via `twister migrate` 5 11 - `api`: public HTTP service 6 12 - `indexer`: private Tap consumer 7 13 - `tap`: private Indigo Tap service 8 - 9 - PostgreSQL is a separate Coolify-managed resource. 10 - 11 - ## Files 12 - 13 - - production compose: `docker-compose.prod.yaml` 14 - - local dev compose: `docker-compose.dev.yaml` 15 - - app image build: `packages/api/Dockerfile` 16 - - Tap image: `ghcr.io/bluesky-social/indigo/tap:sha-4f47add43060c27e8a37d9d76482ecddf001fcd8` 14 + - `llama-embeddings`: private llama.cpp server for future embedding work 17 15 18 16 ## Prerequisites 19 17 20 - - Coolify access 21 - - one Coolify PostgreSQL resource 22 - - this repo connected to Coolify 18 + - one VPS or Coolify host with Docker Compose 19 + - this repo available to the host 23 20 - explicit `INDEXED_COLLECTIONS` and `READ_THROUGH_COLLECTIONS` 24 - - one shared Tap admin password 21 + - one shared `TAP_AUTH_PASSWORD` 22 + - a production `POSTGRES_PASSWORD` 25 23 26 - ## Provision PostgreSQL 24 + Use explicit search collections. Do not use `sh.tangled.*` in production. 27 25 28 - Create the PostgreSQL resource first. 26 + ## Environment 29 27 30 - - keep the generated connection string in Coolify secrets as `DATABASE_URL` 31 - - use PostgreSQL backups from the database resource 32 - - point both `api` and `indexer` at the same database 28 + Required: 33 29 34 - ## Create The Coolify App 30 + - `POSTGRES_PASSWORD` 31 + - `TAP_AUTH_PASSWORD` 32 + - `INDEXED_COLLECTIONS` 35 33 36 - In Coolify: 34 + Common overrides: 37 35 38 - 1. create a new Application 39 - 2. choose the Docker Compose build pack 40 - 3. point it at this repo 41 - 4. set base directory to `/` 42 - 5. set compose file location to `/docker-compose.prod.yaml` 36 + - `POSTGRES_USER` default `twisted` 37 + - `POSTGRES_DB` default `twisted` 38 + - `LOG_LEVEL=info` 39 + - `LOG_FORMAT=json` 40 + - `READ_THROUGH_MODE=missing` 41 + - `READ_THROUGH_COLLECTIONS=<explicit CSV>` 42 + - `READ_THROUGH_MAX_ATTEMPTS=5` 43 + - `HTTP_BIND_ADDR=:8080` 44 + - `INDEXER_HEALTH_ADDR=:9090` 45 + - `LLAMA_MODEL_REPO=nomic-ai/nomic-embed-text-v1.5-GGUF` 46 + - `LLAMA_MODEL_FILE=nomic-embed-text-v1.5.Q8_0.gguf` 43 47 44 - Do not add your own Traefik container. Coolify already provides the proxy. 48 + `llama-embeddings` is private and not part of search yet. It only keeps the 49 + model warm and cached for later adapter work. 45 50 46 - ## Set Environment Variables 51 + ## Bootstrap 47 52 48 - Shared: 53 + From the repo root: 49 54 50 - - `DATABASE_URL` 51 - - `INDEXED_COLLECTIONS` 52 - - `LOG_LEVEL=info` 53 - - `LOG_FORMAT=json` 54 - - `TAP_AUTH_PASSWORD=<required>` 55 + ```sh 56 + just vps-up 57 + ``` 55 58 56 - `api`: 59 + That sequence: 57 60 58 - - `HTTP_BIND_ADDR=:8080` 59 - - `SEARCH_DEFAULT_LIMIT=20` 60 - - `SEARCH_MAX_LIMIT=100` 61 - - `READ_THROUGH_MODE=missing` 62 - - `READ_THROUGH_COLLECTIONS=<explicit CSV>` 63 - - `READ_THROUGH_MAX_ATTEMPTS=5` 64 - - `ENABLE_ADMIN_ENDPOINTS=false` 65 - - `ADMIN_AUTH_TOKEN=<optional>` 66 - - `OAUTH_CLIENT_ID=<optional>` 67 - - `OAUTH_REDIRECT_URIS=<optional CSV>` 61 + 1. starts PostgreSQL and Tap 62 + 2. runs `migrate` 63 + 3. starts `api`, `indexer`, and `llama-embeddings` 68 64 69 - `indexer`: 65 + ## Clean Reset 70 66 71 - - `INDEXER_HEALTH_ADDR=:9090` 72 - - `TAP_URL=ws://tap:2480/channel` 73 - - `ENABLE_INGEST_ENRICHMENT=true` 67 + For a fresh VM or full reset: 74 68 75 - `tap`: 69 + ```sh 70 + just vps-reset 71 + ``` 76 72 77 - - `TAP_COLLECTION_FILTERS=<optional explicit CSV>` 78 - - optional persistent volume override if you do not want the default `/data` 73 + This removes named volumes, recreates PostgreSQL, reapplies migrations, and 74 + starts the full stack from zero. 75 + 76 + ## Coolify Notes 79 77 80 - Use explicit search collections. Do not use `sh.tangled.*` in production. 78 + If you run this through Coolify: 81 79 82 - ## Domains And Health Checks 80 + 1. create one Docker Compose application 81 + 2. point it at `/docker-compose.prod.yaml` 82 + 3. set the env vars above in Coolify 83 + 4. expose only the `api` service through Traefik 83 84 84 - Expose only `api` publicly. 85 + Do not add a separate PostgreSQL resource for this stack. 85 86 86 - - assign the domain in Coolify to the `api` service 87 - - if `api` stays on `:8080`, include that internal port in the Coolify mapping 88 - - configure readiness checks against `GET /readyz` 89 - - keep `indexer` and `tap` private 90 - - monitor `indexer` with `GET /health` 87 + ## Checks 91 88 92 - ## First Bootstrap 89 + - `api`: `GET /readyz` returns `200` 90 + - `indexer`: `GET /health` returns `200` 91 + - `indexer` can reach `ws://tap:2480/channel` 92 + - `llama-embeddings` serves embeddings on its internal port 93 93 94 - 1. deploy `tap` 95 - 2. deploy `api` 96 - 3. deploy `indexer` 97 - 4. confirm `api` returns `200` from `/readyz` 98 - 5. confirm `indexer` returns `200` from `/health` 99 - 6. confirm `indexer` can reach `ws://tap:2480/channel` 100 - 7. open a Coolify terminal in the `indexer` service and run: 94 + Then rebuild the serving dataset: 101 95 102 96 ```sh 103 - twister backfill 104 - twister enrich 105 - twister reindex 97 + docker compose -f docker-compose.prod.yaml exec indexer twister backfill 98 + docker compose -f docker-compose.prod.yaml exec indexer twister enrich 99 + docker compose -f docker-compose.prod.yaml exec indexer twister reindex 106 100 ``` 107 101 108 - This rebuilds the serving dataset from authoritative sources. Do not import the 109 - old Turso data as the default migration path. 102 + Do not import old Turso data as the default migration path. 110 103 111 - ## Point The App At Coolify 104 + ## App Target 112 105 113 106 For local app builds: 114 107 ··· 116 109 VITE_TWISTER_API_BASE_URL=https://<your-api-domain> 117 110 ``` 118 111 119 - Then run the app normally with `pnpm --dir apps/twisted dev` or `build`. 112 + ## Rollback 120 113 121 - ## Rollback Notes 122 - 123 - - keep the SQLite `--local` path only as a temporary development fallback 124 - - rollback production by restoring PostgreSQL and redeploying the prior app 125 - - treat PostgreSQL restore as the database rollback primitive 114 + - PostgreSQL restore is the rollback primitive 115 + - keep `--local` only as a temporary development fallback
+7 -3
docs/reference/resync.md
··· 3 3 updated: 2026-03-26 4 4 --- 5 5 6 - Twisted has three recovery tools. Choose based on what broke. 6 + Twisted has four recovery tools. Choose based on what broke. 7 7 8 8 | Situation | Recovery path | 9 9 | --- | --- | 10 10 | Search results wrong but documents exist | `twister reindex` | 11 11 | Documents missing because Tap never delivered them | `twister backfill` | 12 12 | Documents exist but derived metadata is empty or stale | `twister enrich` | 13 - | Full database loss or migration to a fresh PostgreSQL instance | backfill, enrich, reindex | 13 + | Full database loss or migration to a fresh PostgreSQL instance | migrate, backfill, enrich, reindex | 14 14 15 15 ## Commands 16 + 17 + ### `twister migrate` 18 + 19 + Applies the embedded schema migrations for the configured database. 16 20 17 21 ### `twister indexer` 18 22 ··· 89 93 Use this after restoring to a fresh database or moving to a new PostgreSQL 90 94 instance. 91 95 92 - 1. start `api` once so migrations run 96 + 1. run `twister migrate` 93 97 2. start `indexer` 94 98 3. run `twister backfill` 95 99 4. run `twister enrich`
+12
docs/roadmap.md
··· 56 56 - [ ] Structured metrics: ingestion rate, search latency, embedding throughput 57 57 - [ ] Dashboard or log-based monitoring 58 58 59 + ## API: Embedding Adapter 60 + 61 + **Depends on:** API: Search Stabilization 62 + 63 + - [ ] Add an embedding provider interface in the API 64 + - [ ] Add a `llama.cpp` HTTP adapter targeting `llama-embeddings` 65 + - [ ] Add PostgreSQL embedding schema and jobs using `pgvector` 66 + - [ ] Define source-code chunking and repo re-embed triggers 67 + - [ ] Add `embed` and `reembed` operational commands 68 + - [ ] Add queue-depth, latency, and throughput metrics for embeddings 69 + - [ ] Keep semantic or hybrid retrieval behind a non-default search mode 70 + 59 71 ## App: Search & Discovery 60 72 61 73 Wire the Explore tab to the search API and add activity feed.
+2 -1
docs/specs/search.md
··· 10 10 11 11 - primary storage: PostgreSQL 12 12 - local default URL: `postgresql://localhost/${USER}_dev?sslmode=disable` 13 - - production deploy target: Coolify application plus managed PostgreSQL 13 + - production deploy target: `docker-compose.prod.yaml` on a VPS or Coolify host 14 14 - legacy fallback: local SQLite behind `--local` 15 + - `llama-embeddings` is deployed only as future embedding groundwork 15 16 16 17 ## Goals 17 18
+18
justfile
··· 49 49 api-run-indexer mode="local": 50 50 just --justfile packages/api/justfile run-indexer {{mode}} 51 51 52 + vps-up: 53 + docker compose -f docker-compose.prod.yaml up -d postgres tap 54 + docker compose -f docker-compose.prod.yaml up --build migrate --exit-code-from migrate 55 + docker compose -f docker-compose.prod.yaml up -d --build api indexer llama-embeddings 56 + 57 + vps-down: 58 + docker compose -f docker-compose.prod.yaml down 59 + 60 + vps-migrate: 61 + docker compose -f docker-compose.prod.yaml up -d postgres 62 + docker compose -f docker-compose.prod.yaml up --build migrate --exit-code-from migrate 63 + 64 + vps-reset: 65 + docker compose -f docker-compose.prod.yaml down -v 66 + docker compose -f docker-compose.prod.yaml up -d postgres 67 + docker compose -f docker-compose.prod.yaml up --build migrate --exit-code-from migrate 68 + docker compose -f docker-compose.prod.yaml up -d --build tap api indexer llama-embeddings 69 + 52 70 db-up: 53 71 docker compose -f docker-compose.dev.yaml up -d postgres tap 54 72
+8 -2
packages/api/doc.go
··· 9 9 // 10 10 // cd /Users/owais/Projects/Twisted 11 11 // just db-up 12 + // just api-build 13 + // DATABASE_URL=postgresql://localhost/${USER}_dev?sslmode=disable ./packages/api/twister migrate 12 14 // just api-dev 13 15 // just api-run-indexer 14 16 // ··· 54 56 // 55 57 // twister api 56 58 // twister indexer 59 + // twister migrate 57 60 // twister backfill 58 61 // twister reindex 59 62 // twister enrich ··· 61 64 // 62 65 // # Deployment 63 66 // 64 - // Production uses Coolify for the `api`, `indexer`, and `tap` services plus a 65 - // separate Coolify-managed PostgreSQL resource. See docs/reference/deployment-walkthrough.md. 67 + // Production uses docker-compose.prod.yaml as the source-of-truth VPS stack: 68 + // PostgreSQL, migrate, api, indexer, tap, and llama-embeddings. 69 + // 70 + // Semantic search is still deferred. The llama.cpp service is present only as 71 + // operational groundwork for a later embedding adapter. 66 72 package main
-1
packages/api/internal/embed/embed.go
··· 1 - package embed
+29 -20
packages/api/main.go
··· 47 47 root.AddCommand( 48 48 newAPICmd(&local), 49 49 newIndexerCmd(&local), 50 + newMigrateCmd(&local), 50 51 newBackfillCmd(&local), 51 52 newReindexCmd(&local), 52 53 newEnrichCmd(&local), ··· 88 89 return fmt.Errorf("open database: %w", err) 89 90 } 90 91 defer db.Close() 91 - 92 - if err := store.Migrate(db, cfg.DatabaseURL); err != nil { 93 - return fmt.Errorf("migrate database: %w", err) 94 - } 95 92 96 93 st := store.New(cfg.DatabaseURL, db) 97 94 searchRepo := search.NewRepository(cfg.DatabaseURL, db) ··· 147 144 } 148 145 defer db.Close() 149 146 150 - if err := store.Migrate(db, cfg.DatabaseURL); err != nil { 151 - return fmt.Errorf("migrate database: %w", err) 152 - } 153 - 154 147 st := store.New(cfg.DatabaseURL, db) 155 148 registry := normalize.NewRegistry() 156 149 tap := tapclient.New(cfg.TapURL, cfg.TapAuthPassword, log) ··· 207 200 } 208 201 } 209 202 203 + func newMigrateCmd(local *bool) *cobra.Command { 204 + return &cobra.Command{ 205 + Use: "migrate", 206 + Short: "Apply database schema migrations", 207 + RunE: func(cmd *cobra.Command, args []string) error { 208 + cfg, err := config.Load(config.LoadOptions{Local: *local}) 209 + if err != nil { 210 + return fmt.Errorf("config: %w", err) 211 + } 212 + log := observability.NewLogger(cfg) 213 + log.Info("starting migrate", slog.String("service", "migrate"), slog.String("version", version)) 214 + 215 + db, err := store.Open(cfg.DatabaseURL) 216 + if err != nil { 217 + return fmt.Errorf("open database: %w", err) 218 + } 219 + defer db.Close() 220 + 221 + if err := store.Migrate(db, cfg.DatabaseURL); err != nil { 222 + return fmt.Errorf("migrate database: %w", err) 223 + } 224 + 225 + log.Info("migrate finished") 226 + return nil 227 + }, 228 + } 229 + } 230 + 210 231 func newBackfillCmd(local *bool) *cobra.Command { 211 232 var opts backfill.Options 212 233 ··· 231 252 } 232 253 defer db.Close() 233 254 234 - if err := store.Migrate(db, cfg.DatabaseURL); err != nil { 235 - return fmt.Errorf("migrate database: %w", err) 236 - } 237 - 238 255 tapAdmin, err := backfill.NewHTTPTapAdmin(cfg.TapURL, cfg.TapAuthPassword) 239 256 if err != nil { 240 257 return fmt.Errorf("tap admin client: %w", err) ··· 299 316 } 300 317 defer db.Close() 301 318 302 - if err := store.Migrate(db, cfg.DatabaseURL); err != nil { 303 - return fmt.Errorf("migrate database: %w", err) 304 - } 305 - 306 319 ctx, cancel := baseContext() 307 320 defer cancel() 308 321 ··· 346 359 return fmt.Errorf("open database: %w", err) 347 360 } 348 361 defer db.Close() 349 - 350 - if err := store.Migrate(db, cfg.DatabaseURL); err != nil { 351 - return fmt.Errorf("migrate database: %w", err) 352 - } 353 362 354 363 xrpcClient := xrpc.NewClient( 355 364 xrpc.WithPLCDirectory(cfg.PLCDirectoryURL),