···4242just db-up
4343```
44444545+Bootstrap the schema once:
4646+4747+```bash
4848+just api-build
4949+DATABASE_URL="postgresql://localhost/${USER}_dev?sslmode=disable" \
5050+ ./packages/api/twister migrate
5151+```
5252+4553Run the mobile app:
46544755```bash
···79878088## Deployment
81898282-Production deployment now uses Coolify plus a separate Coolify-managed
8383-PostgreSQL instance. The backend services are defined in
8484-`docker-compose.prod.yaml`.
9090+`docker-compose.prod.yaml` is now the source-of-truth VPS stack. It runs
9191+`postgres`, `migrate`, `api`, `indexer`, `tap`, and `llama-embeddings`.
9292+9393+The llama.cpp service is only deployment groundwork for a later embedding
9494+adapter. Search remains keyword-only for now.
85958696See [`docs/reference/deployment-walkthrough.md`](docs/reference/deployment-walkthrough.md)
8797for the full setup, bootstrap, backup, and cutover flow.
···77## Decision
8899Twisted will use PostgreSQL as the primary database backend for search,
1010-indexing, queue state, and activity cache. The production deploy target is a
1111-Coolify application for `api`, `indexer`, and `tap` plus a separate
1212-Coolify-managed PostgreSQL instance.
1010+indexing, queue state, and activity cache. The production deploy target is one
1111+Compose stack for `postgres`, `migrate`, `api`, `indexer`, `tap`, and
1212+`llama-embeddings`. Coolify can host that stack, but the Compose file remains
1313+the source of truth.
13141415## Why
15161617- PostgreSQL is the better fit for long-running multi-service deployment.
1717-- Coolify gives the project a straightforward Git-to-deploy path with built-in
1818- Traefik and a managed database resource.
1818+- Coolify still gives the project a straightforward Git-to-deploy path with
1919+ built-in Traefik when we want it.
1920- The current service shape already wants two long-lived processes writing to
2021 one shared database.
2122- A local PostgreSQL workflow keeps development closer to production than the
···2829- one mainstream database for local and remote environments
2930- simpler production backups and restore story
3031- easier operational model for `api` and `indexer`
3232+- explicit migration ownership through a one-shot `migrate` command
3133- no dependency on Turso-specific SQLite extension behavior
32343335### Negative
···53551. add PostgreSQL connection/config support and local defaults
54562. add a primary PostgreSQL migration set
55573. move search and store implementations to PostgreSQL
5656-4. deploy `api`, `indexer`, and `tap` from `docker-compose.prod.yaml`
5858+4. deploy the Compose stack from `docker-compose.prod.yaml`
57595. rebuild data through `backfill`, `enrich`, and `reindex`
58606. cut traffic over only after smoke checks pass
5961
+12-4
docs/reference/api.md
···1313| --- | --- |
1414| `api` | HTTP API server |
1515| `indexer` | Tap consumer and index writer |
1616+| `migrate` | apply embedded SQL migrations |
1617| `backfill` | register repos with Tap |
1718| `enrich` | fill missing repo names, handles, and web URLs |
1819| `reindex` | re-upsert documents and finalize the search index |
···5758`documents` stores a generated weighted `tsvector` column plus a GIN index for
5859keyword search.
59606161+No embedding tables are active yet. `llama-embeddings` is deployed only as
6262+infra groundwork for a later semantic-search milestone.
6363+6064## Configuration
61656266Primary env vars:
···88928993```sh
9094just db-up
9595+just api-build
9696+DATABASE_URL="postgresql://localhost/${USER}_dev?sslmode=disable" \
9797+ ./packages/api/twister migrate
9198just api-dev
9299just api-run-indexer
93100```
9410195102That dev compose file also runs Tap locally at `ws://localhost:2480/channel`.
103103+`api` and `indexer` no longer auto-apply schema changes on startup.
9610497105Use `just api-dev sqlite` only when you need the temporary SQLite rollback path.
98106···100108101109Production uses:
102110103103-- Coolify Application with `docker-compose.prod.yaml`
104104-- separate Coolify-managed PostgreSQL resource
105105-- private Tap service from the pinned Indigo image
106106-- built-in Coolify Traefik for the public `api` domain
111111+- `docker-compose.prod.yaml` as the VPS stack source of truth
112112+- in-stack PostgreSQL on `pgvector/pgvector:pg17`
113113+- a one-shot `migrate` service before long-lived services start
114114+- private Tap and llama.cpp embedding services on the internal Compose network
107115108116See `docs/reference/deployment-walkthrough.md` for the full production flow.
+70-80
docs/reference/deployment-walkthrough.md
···11# Deployment Walkthrough
2233-Twisted deploys to Coolify as one Compose application with three services:
33+Twisted now deploys as one VPS Compose stack defined by
44+`docker-compose.prod.yaml`. Coolify can still host that stack, but the Compose
55+file is the source of truth.
4677+## Services
88+99+- `postgres`: primary database on `pgvector/pgvector:pg17`
1010+- `migrate`: one-shot schema bootstrap via `twister migrate`
511- `api`: public HTTP service
612- `indexer`: private Tap consumer
713- `tap`: private Indigo Tap service
88-99-PostgreSQL is a separate Coolify-managed resource.
1010-1111-## Files
1212-1313-- production compose: `docker-compose.prod.yaml`
1414-- local dev compose: `docker-compose.dev.yaml`
1515-- app image build: `packages/api/Dockerfile`
1616-- Tap image: `ghcr.io/bluesky-social/indigo/tap:sha-4f47add43060c27e8a37d9d76482ecddf001fcd8`
1414+- `llama-embeddings`: private llama.cpp server for future embedding work
17151816## Prerequisites
19172020-- Coolify access
2121-- one Coolify PostgreSQL resource
2222-- this repo connected to Coolify
1818+- one VPS or Coolify host with Docker Compose
1919+- this repo available to the host
2320- explicit `INDEXED_COLLECTIONS` and `READ_THROUGH_COLLECTIONS`
2424-- one shared Tap admin password
2121+- one shared `TAP_AUTH_PASSWORD`
2222+- a production `POSTGRES_PASSWORD`
25232626-## Provision PostgreSQL
2424+Use explicit search collections. Do not use `sh.tangled.*` in production.
27252828-Create the PostgreSQL resource first.
2626+## Environment
29273030-- keep the generated connection string in Coolify secrets as `DATABASE_URL`
3131-- use PostgreSQL backups from the database resource
3232-- point both `api` and `indexer` at the same database
2828+Required:
33293434-## Create The Coolify App
3030+- `POSTGRES_PASSWORD`
3131+- `TAP_AUTH_PASSWORD`
3232+- `INDEXED_COLLECTIONS`
35333636-In Coolify:
3434+Common overrides:
37353838-1. create a new Application
3939-2. choose the Docker Compose build pack
4040-3. point it at this repo
4141-4. set base directory to `/`
4242-5. set compose file location to `/docker-compose.prod.yaml`
3636+- `POSTGRES_USER` default `twisted`
3737+- `POSTGRES_DB` default `twisted`
3838+- `LOG_LEVEL=info`
3939+- `LOG_FORMAT=json`
4040+- `READ_THROUGH_MODE=missing`
4141+- `READ_THROUGH_COLLECTIONS=<explicit CSV>`
4242+- `READ_THROUGH_MAX_ATTEMPTS=5`
4343+- `HTTP_BIND_ADDR=:8080`
4444+- `INDEXER_HEALTH_ADDR=:9090`
4545+- `LLAMA_MODEL_REPO=nomic-ai/nomic-embed-text-v1.5-GGUF`
4646+- `LLAMA_MODEL_FILE=nomic-embed-text-v1.5.Q8_0.gguf`
43474444-Do not add your own Traefik container. Coolify already provides the proxy.
4848+`llama-embeddings` is private and not part of search yet. It only keeps the
4949+model warm and cached for later adapter work.
45504646-## Set Environment Variables
5151+## Bootstrap
47524848-Shared:
5353+From the repo root:
49545050-- `DATABASE_URL`
5151-- `INDEXED_COLLECTIONS`
5252-- `LOG_LEVEL=info`
5353-- `LOG_FORMAT=json`
5454-- `TAP_AUTH_PASSWORD=<required>`
5555+```sh
5656+just vps-up
5757+```
55585656-`api`:
5959+That sequence:
57605858-- `HTTP_BIND_ADDR=:8080`
5959-- `SEARCH_DEFAULT_LIMIT=20`
6060-- `SEARCH_MAX_LIMIT=100`
6161-- `READ_THROUGH_MODE=missing`
6262-- `READ_THROUGH_COLLECTIONS=<explicit CSV>`
6363-- `READ_THROUGH_MAX_ATTEMPTS=5`
6464-- `ENABLE_ADMIN_ENDPOINTS=false`
6565-- `ADMIN_AUTH_TOKEN=<optional>`
6666-- `OAUTH_CLIENT_ID=<optional>`
6767-- `OAUTH_REDIRECT_URIS=<optional CSV>`
6161+1. starts PostgreSQL and Tap
6262+2. runs `migrate`
6363+3. starts `api`, `indexer`, and `llama-embeddings`
68646969-`indexer`:
6565+## Clean Reset
70667171-- `INDEXER_HEALTH_ADDR=:9090`
7272-- `TAP_URL=ws://tap:2480/channel`
7373-- `ENABLE_INGEST_ENRICHMENT=true`
6767+For a fresh VM or full reset:
74687575-`tap`:
6969+```sh
7070+just vps-reset
7171+```
76727777-- `TAP_COLLECTION_FILTERS=<optional explicit CSV>`
7878-- optional persistent volume override if you do not want the default `/data`
7373+This removes named volumes, recreates PostgreSQL, reapplies migrations, and
7474+starts the full stack from zero.
7575+7676+## Coolify Notes
79778080-Use explicit search collections. Do not use `sh.tangled.*` in production.
7878+If you run this through Coolify:
81798282-## Domains And Health Checks
8080+1. create one Docker Compose application
8181+2. point it at `/docker-compose.prod.yaml`
8282+3. set the env vars above in Coolify
8383+4. expose only the `api` service through Traefik
83848484-Expose only `api` publicly.
8585+Do not add a separate PostgreSQL resource for this stack.
85868686-- assign the domain in Coolify to the `api` service
8787-- if `api` stays on `:8080`, include that internal port in the Coolify mapping
8888-- configure readiness checks against `GET /readyz`
8989-- keep `indexer` and `tap` private
9090-- monitor `indexer` with `GET /health`
8787+## Checks
91889292-## First Bootstrap
8989+- `api`: `GET /readyz` returns `200`
9090+- `indexer`: `GET /health` returns `200`
9191+- `indexer` can reach `ws://tap:2480/channel`
9292+- `llama-embeddings` serves embeddings on its internal port
93939494-1. deploy `tap`
9595-2. deploy `api`
9696-3. deploy `indexer`
9797-4. confirm `api` returns `200` from `/readyz`
9898-5. confirm `indexer` returns `200` from `/health`
9999-6. confirm `indexer` can reach `ws://tap:2480/channel`
100100-7. open a Coolify terminal in the `indexer` service and run:
9494+Then rebuild the serving dataset:
1019510296```sh
103103-twister backfill
104104-twister enrich
105105-twister reindex
9797+docker compose -f docker-compose.prod.yaml exec indexer twister backfill
9898+docker compose -f docker-compose.prod.yaml exec indexer twister enrich
9999+docker compose -f docker-compose.prod.yaml exec indexer twister reindex
106100```
107101108108-This rebuilds the serving dataset from authoritative sources. Do not import the
109109-old Turso data as the default migration path.
102102+Do not import old Turso data as the default migration path.
110103111111-## Point The App At Coolify
104104+## App Target
112105113106For local app builds:
114107···116109VITE_TWISTER_API_BASE_URL=https://<your-api-domain>
117110```
118111119119-Then run the app normally with `pnpm --dir apps/twisted dev` or `build`.
112112+## Rollback
120113121121-## Rollback Notes
122122-123123-- keep the SQLite `--local` path only as a temporary development fallback
124124-- rollback production by restoring PostgreSQL and redeploying the prior app
125125-- treat PostgreSQL restore as the database rollback primitive
114114+- PostgreSQL restore is the rollback primitive
115115+- keep `--local` only as a temporary development fallback
+7-3
docs/reference/resync.md
···33updated: 2026-03-26
44---
5566-Twisted has three recovery tools. Choose based on what broke.
66+Twisted has four recovery tools. Choose based on what broke.
7788| Situation | Recovery path |
99| --- | --- |
1010| Search results wrong but documents exist | `twister reindex` |
1111| Documents missing because Tap never delivered them | `twister backfill` |
1212| Documents exist but derived metadata is empty or stale | `twister enrich` |
1313-| Full database loss or migration to a fresh PostgreSQL instance | backfill, enrich, reindex |
1313+| Full database loss or migration to a fresh PostgreSQL instance | migrate, backfill, enrich, reindex |
14141515## Commands
1616+1717+### `twister migrate`
1818+1919+Applies the embedded schema migrations for the configured database.
16201721### `twister indexer`
1822···8993Use this after restoring to a fresh database or moving to a new PostgreSQL
9094instance.
91959292-1. start `api` once so migrations run
9696+1. run `twister migrate`
93972. start `indexer`
94983. run `twister backfill`
95994. run `twister enrich`
+12
docs/roadmap.md
···5656- [ ] Structured metrics: ingestion rate, search latency, embedding throughput
5757- [ ] Dashboard or log-based monitoring
58585959+## API: Embedding Adapter
6060+6161+**Depends on:** API: Search Stabilization
6262+6363+- [ ] Add an embedding provider interface in the API
6464+- [ ] Add a `llama.cpp` HTTP adapter targeting `llama-embeddings`
6565+- [ ] Add PostgreSQL embedding schema and jobs using `pgvector`
6666+- [ ] Define source-code chunking and repo re-embed triggers
6767+- [ ] Add `embed` and `reembed` operational commands
6868+- [ ] Add queue-depth, latency, and throughput metrics for embeddings
6969+- [ ] Keep semantic or hybrid retrieval behind a non-default search mode
7070+5971## App: Search & Discovery
60726173Wire the Explore tab to the search API and add activity feed.
+2-1
docs/specs/search.md
···10101111- primary storage: PostgreSQL
1212- local default URL: `postgresql://localhost/${USER}_dev?sslmode=disable`
1313-- production deploy target: Coolify application plus managed PostgreSQL
1313+- production deploy target: `docker-compose.prod.yaml` on a VPS or Coolify host
1414- legacy fallback: local SQLite behind `--local`
1515+- `llama-embeddings` is deployed only as future embedding groundwork
15161617## Goals
1718
+18
justfile
···4949api-run-indexer mode="local":
5050 just --justfile packages/api/justfile run-indexer {{mode}}
51515252+vps-up:
5353+ docker compose -f docker-compose.prod.yaml up -d postgres tap
5454+ docker compose -f docker-compose.prod.yaml up --build migrate --exit-code-from migrate
5555+ docker compose -f docker-compose.prod.yaml up -d --build api indexer llama-embeddings
5656+5757+vps-down:
5858+ docker compose -f docker-compose.prod.yaml down
5959+6060+vps-migrate:
6161+ docker compose -f docker-compose.prod.yaml up -d postgres
6262+ docker compose -f docker-compose.prod.yaml up --build migrate --exit-code-from migrate
6363+6464+vps-reset:
6565+ docker compose -f docker-compose.prod.yaml down -v
6666+ docker compose -f docker-compose.prod.yaml up -d postgres
6767+ docker compose -f docker-compose.prod.yaml up --build migrate --exit-code-from migrate
6868+ docker compose -f docker-compose.prod.yaml up -d --build tap api indexer llama-embeddings
6969+5270db-up:
5371 docker compose -f docker-compose.dev.yaml up -d postgres tap
5472
+8-2
packages/api/doc.go
···99//
1010// cd /Users/owais/Projects/Twisted
1111// just db-up
1212+// just api-build
1313+// DATABASE_URL=postgresql://localhost/${USER}_dev?sslmode=disable ./packages/api/twister migrate
1214// just api-dev
1315// just api-run-indexer
1416//
···5456//
5557// twister api
5658// twister indexer
5959+// twister migrate
5760// twister backfill
5861// twister reindex
5962// twister enrich
···6164//
6265// # Deployment
6366//
6464-// Production uses Coolify for the `api`, `indexer`, and `tap` services plus a
6565-// separate Coolify-managed PostgreSQL resource. See docs/reference/deployment-walkthrough.md.
6767+// Production uses docker-compose.prod.yaml as the source-of-truth VPS stack:
6868+// PostgreSQL, migrate, api, indexer, tap, and llama-embeddings.
6969+//
7070+// Semantic search is still deferred. The llama.cpp service is present only as
7171+// operational groundwork for a later embedding adapter.
6672package main