···100100- [x] Handle normalization failures: log, skip, advance cursor
101101- [x] Handle DB failures: retry with backoff, do not advance cursor
102102103103-### Verification
104104-105105-- [ ] Indexer connects to Tap via WebSocket in development
106106-- [ ] A newly created tracked record appears in `documents` table
107107-- [ ] An updated record changes the existing row (CID changes)
108108-- [ ] A delete event tombstones the row (`deleted_at` set)
109109-- [ ] Killing and restarting the indexer resumes from persisted cursor without duplication
110110-- [ ] Identity events update handle cache
111111-- [ ] Unsupported collections are silently skipped
112112-- [ ] Connection drops trigger automatic reconnection
113113-114103### Exit Criteria
115104116105The system continuously ingests and persists `sh.tangled.*` records from Tap.
···179168 - repeat run after expanding the seed list
180169 - dry-run before production mutation
181170 - Implemented: `packages/api/internal/backfill/doc.go`
182182-183183-### Verification
184184-185185-- [ ] A small seed file of known Tangled users produces a non-empty discovery graph
186186-- [ ] `--max-hops 1` limits discovery to direct neighbors
187187-- [ ] `--dry-run` does not call Tap mutation endpoints
188188-- [ ] Already-tracked DIDs are reported and not re-submitted unnecessarily
189189-- [ ] Re-running the same seeds is effectively idempotent
190190-- [ ] Newly submitted DIDs cause Tap to begin historical backfill
191191-- [ ] Search results become materially richer after bootstrap than they were under live-only ingestion
192171193172### Exit Criteria
194173···247226- [x] Add request logging middleware (method, path, status, duration)
248227- [x] Add CORS headers if needed
249228250250-### Verification
251251-252252-- [ ] Searching by exact repo name returns the expected repo first
253253-- [ ] Searching by title term returns expected documents
254254-- [ ] Searching by author handle returns relevant docs
255255-- [ ] Tombstoned documents do not appear
256256-- [ ] Malformed query parameters return 400 with error JSON
257257-- [ ] DB outage causes `/readyz` to fail (503)
258258-- [ ] Pagination works: `offset=0&limit=5` then `offset=5&limit=5` returns different results
259259-- [ ] Filter by collection returns only matching docs
260260-261229### Exit Criteria
262230263231A user can search Tangled content reliably with keyword search.
···302270- [x] Result card links open canonical Tangled URLs in new tab
303271- [x] Verify total site weight under 50 KB (excluding fonts and Alpine CDN) — 21 KB total
304272305305-### Verification
306306-307307-- [ ] `twister api` serves the search page at `http://localhost:8080/`
308308-- [ ] API endpoints (`/search`, `/healthz`, etc.) still work alongside the site
309309-- [ ] Searching a known repo name shows it in results
310310-- [ ] Filter by type restricts results to that type
311311-- [ ] "Load more" appends next page of results
312312-- [ ] API docs pages render correct endpoint signatures, parameter tables, and example JSON
313313-- [ ] Site works on mobile viewport (stacked layout at 640px)
314314-- [ ] Site works with API unavailable (error state shown, no crash)
315315-- [ ] All pages share consistent styling and navigation
316316-317273### Exit Criteria
318274319275A user can search Tangled content and read API docs from a public URL without installing anything.
···352308- [x] Test graceful shutdown on redeploy (SIGTERM handling)
353309- [x] Document deploy steps
354310355355-### Verification
356356-357357-- [ ] API service becomes healthy and routable (public URL)
358358-- [ ] Indexer service starts and stays healthy
359359-- [ ] A new Tangled record ingested post-deploy becomes searchable
360360-- [ ] A redeploy preserves API availability
361361-- [ ] A restart does not lose sync position (cursor persisted)
362362-- [ ] Health checks correctly report status
363363-364311### Exit Criteria
365312366313The system runs as a deployed service with health-checked processes on Railway.
367314368368-## M7 — Reindex and Repair
315315+## M7 — Reindex and Repair ✅
369316370317refs: [specs/05-search.md](../specs/05-search.md)
371318···377324378325- `twister reindex` command with scoping options
379326- Dry-run mode
380380-- Admin reindex endpoint (optional)
327327+- Admin reindex endpoint
381328- Progress logging and error summary
382329383330### Tasks
384331385385-- [ ] Implement `reindex` subcommand with flags:
332332+- [x] Implement `reindex` subcommand with flags:
386333 - `--collection` — reindex one collection
387334 - `--did` — reindex one DID's documents
388335 - `--document` — reindex one document by ID
389336 - `--dry-run` — show intended work without writes
390337 - No flags → reindex all
391391-- [ ] Implement reindex logic:
338338+- [x] Implement reindex logic:
392339 1. Select documents matching scope
393340 2. For each document, re-run normalization from stored fields (or re-fetch if source available)
394341 3. Update FTS-relevant fields
395342 4. Upsert back to store
396343 5. Run `OPTIMIZE INDEX idx_documents_fts` after bulk reindex to merge Tantivy segments
397344 6. Log progress (N/total, errors)
398398-- [ ] Implement `POST /admin/reindex` endpoint (behind `ENABLE_ADMIN_ENDPOINTS` + `ADMIN_AUTH_TOKEN`)
399399-- [ ] Add error summary output on completion
400400-- [ ] Exit non-zero on unrecoverable failures
401401-402402-### Verification
403403-404404-- [ ] Reindexing one document updates its stored normalized text
405405-- [ ] Reindexing one collection repairs intentionally corrupted rows
406406-- [ ] Dry-run shows intended work without writes
407407-- [ ] Reindex command exits non-zero on failures
408408-- [ ] Admin endpoint triggers reindex when enabled
345345+- [x] Implement `POST /admin/reindex` endpoint (behind `ENABLE_ADMIN_ENDPOINTS` + `ADMIN_AUTH_TOKEN`)
346346+- [x] Add error summary output on completion
347347+- [x] Exit non-zero on unrecoverable failures
409348410349### Exit Criteria
411350···443382 - Reindex procedure
444383 - Backfill notes
445384 - Failure triage guide
446446-447447-### Verification
448448-449449-- [ ] A failed Tap decode surfaces enough context to debug (collection, DID, rkey, error class)
450450-- [ ] DB connectivity failures are visible in logs and readiness
451451-- [ ] Operator can follow the runbook to diagnose a broken indexer
452452-- [ ] Search latency is logged per request
453385454386### Exit Criteria
455387
···369369 }
370370371371 handle := res.profile.Handle
372372- // Prefer the seed handle if the user was specified by handle in seeds.
373372 if h, ok := seedHandles[res.did]; ok && h != "" {
374373 handle = h
375374 }
···386385 }
387386 }
388387389389- // Only create a document if we got a profile record back.
390388 if res.profile.Record == nil {
391389 continue
392390 }
-2
packages/api/internal/backfill/backfill_test.go
···310310 t.Fatalf("run backfill: %v", err)
311311 }
312312313313- // Identity handle should be persisted.
314313 if st.identities["did:plc:seed"] != "alice.tangled.sh" {
315314 t.Fatalf("expected identity handle for seed DID, got %#v", st.identities)
316315 }
317316318318- // Profile document should be created.
319317 if len(st.documents) != 1 {
320318 t.Fatalf("expected 1 profile document, got %d", len(st.documents))
321319 }
-1
packages/api/internal/backfill/profile.go
···6363 defer resp.Body.Close()
64646565 if resp.StatusCode == http.StatusNotFound {
6666- // No profile record — return handle only so identity can still be stored.
6766 return &ProfileRecord{Handle: handle}, nil
6867 }
6968 if resp.StatusCode != http.StatusOK {
-1
packages/api/internal/backfill/seed.go
···6464 return parseSeedFile(input)
6565 }
66666767- // Single inline DID/handle is supported for convenience.
6867 return parseSeedList(input)
6968}
7069
+1-1
packages/api/internal/config/config.go
···124124 if _, err := os.Stat(candidate); err != nil {
125125 continue
126126 }
127127- // Load does not override existing process env vars.
127127+128128 _ = godotenv.Load(candidate)
129129 }
130130}