How we shipped Anserra from empty repo to production in a week
Engineering notes on the tech stack, the traps that cost time, and what we'd do differently.
Anserra went from an empty repo to production in about a week. This is the honest version of how — and what we’d do differently. Written for the “how do people ship things fast” audience.
The stack
- Next.js 15 (App Router) — server components for pages, server actions for mutations, route handlers for API + widget.
- PostgreSQL 18 + pgvector 0.8.6 — one database for structured data and embeddings. No separate vector DB.
- Prisma 6 — schema-driven, migrations tracked in Git.
- Tailwind CSS 4 + shadcn/ui — design tokens in OKLCH, dense component library.
- AI SDK 4 (Vercel) — provider abstraction for OpenAI, Anthropic, Google. OpenRouter as a universal gateway.
- Auth.js — credentials + optional OAuth. TOTP MFA.
- pnpm for package management, tsx for scripts, Vitest 4 for tests.
No new frameworks. Nothing exotic. The interesting decision was drivers: every external service has a real fallback so the app runs on nothing but Postgres. Mock LLM, hashed embeddings, on-disk storage, inline queue, console email, demo billing. Adding real keys upgrades each independently.
The five decisions that saved time
1. Standalone Next.js output. `output: “standalone”` produces a traced bundle you can put in a Docker image without `node_modules`. Deploy image is < 200MB. `pm2` runs the standalone `server.js` directly.
2. Prisma migrations in the repo. `prisma migrate deploy` on boot. No manual DB migration workflow. Every deploy applies pending migrations exactly once.
3. Server actions for everything mutable. No REST layer for the web app. Actions live next to the routes they mutate. Zod validates every input. Prisma types cover the return.
4. A single `capabilities` object exported from `lib/env.ts`. Every driver picks its implementation from that. Every UI surface reads it to decide whether to say “demo mode.” One source of truth for what’s really configured.
5. Tests that check contracts, not implementations. RBAC, chunker, crypto, entitlements, retrieval fusion, CSV escaping. 59 tests total, ~1 second to run. When we broke the entitlements contract during the OpenRouter refactor, the tests caught it.
The five things that cost time
1. `prisma db seed` runs the DEMO seed by default. The prod deploy script ran it and silently reset the owner’s password to `TestPass123!` and created four teammate accounts nobody wanted. Fix: neuter the seed step in the prod deploy script and write a separate `seed-production.ts` for plan catalogue only. Never trust a package.json seed hook to be prod-safe.
2. OpenRouter’s `/responses` endpoint doesn’t exist. The AI SDK’s OpenAI provider defaults to `/responses` in v4. OpenRouter speaks `/chat/completions`. The error message (“User not found”) pointed nowhere. Fix: `.chat()` on the createOpenAI client explicitly pins the correct endpoint.
3. `streamText` reports errors via `onError`, not throws. A dead provider looked like an empty successful stream. Fix: capture the error via `onError` and check it before returning the empty stream. Otherwise every visitor sees a broken widget.
4. pgvector HNSW’s 2000-dim ceiling. OpenAI’s embeddings are 3072. The obvious fix (drop dimensions) hurts recall. The right fix (halfvec cast) took an hour to understand and 15 minutes to implement. But it silently costs you the index if the query doesn’t cast identically. `EXPLAIN` after every schema change.
5. Traefik `redirectRegex` and here-doc escaping. Our deploy script wrote `replacement: “https://anserra.onzira.com$\${1}”\` because the bash here-doc ate one `$`. In production it 301’d every path to a literal `$1`. Fix: use nowdocs or plain string writes, and always test a real path redirect.
What we’d do differently
- Set up production observability on day one. We ran without structured logging for a week and paid for it in the seed-clobber diagnosis. Should have been Sentry or similar from t=0.
- Write the customer support ticketing loop earlier. We built admin-side ticket viewing but forgot customer-side ticket submission. It shipped in a follow-up. Should have been the same PR.
- Start with a cleaner separation between marketing and app code. Everything ended up in Next.js. Fine for a week; it’ll bite us eventually.
What we won’t change
- Standalone deployment. No hosting-vendor lock-in. The docker image runs anywhere.
- The driver-fallback pattern. Being able to run the whole product on nothing but Postgres is what let us test everything without an OpenAI key.
- Prisma + Postgres + pgvector. One database, structured data + vectors, joins between them. Anything else would have doubled our ops surface.
The one-line summary
Ship fast by removing choices. One database. One provider abstraction. One deployment target. Real fallbacks so nothing blocks. Test the contracts. Ignore what doesn’t matter for a week and pay it back after launch.
If you want to see how we ship — Anserra is public, and this blog is where we write it up.
Read next
- Company
10 things we learned shipping Anserra to production
The lessons that don't fit in code comments — pricing, positioning, onboarding, honesty, and the mistake that cost us three days.
Read - Engineering
Hybrid retrieval, explained: why semantic search alone fails on real customer questions
Semantic embeddings blur exact strings like SKUs and error codes. Hybrid retrieval fixes that — here's how it works and how we implement it.
Read - Engineering
pgvector vs Pinecone in 2026: when each wins
A pragmatic comparison of Postgres pgvector against Pinecone for production RAG. Pricing, latency, filtering, hybrid search and what breaks under load.
Read