Working on Papervine
The contributor contract — definition of done, how to drive the app, the GitOps migration flow, the command reference, and code conventions.
The contributor contract — definition of done, how to drive the app, the GitOps migration flow, the command reference, and code conventions.
Papervine is a multi-tenant docs platform: it renders a docs
site from a Git repo of MDX + a docs.json. This page is the contract for how to work in the repo —
follow it for every change. Start with the architecture overview for the
big picture, and renderer internals for how MDX actually renders.
npm install
cp .env.example .env.local
npm run dev
That’s the whole setup. npm run dev brings up the local services, the app, and the
automations worker together, and every layer it can’t reach degrades on its own — you get a
working docs renderer and dashboard immediately.
The AI features need one more choice, because there’s no sensible default: they stay
switched off (returning a clear “not configured”) until you pick a profile in .env.local.
The free one runs on your own machine:
brew install ollama # or download from ollama.com; Linux: curl -fsSL https://ollama.com/install.sh | sh
ollama pull qwen3.5 # ~6.5 GB, and it's the smallest model that reliably drives our agents
Then uncomment Profile A in .env.local and restart. If you’d rather use a hosted
provider, uncomment Profile B or C instead and add a key. See
Local AI models for what local models can and can’t do well —
briefly: they handle the assistant comfortably, and automation runs slowly but correctly.
Working on the renderer, the dashboard, or anything that isn’t AI? Skip the model setup
entirely. Those surfaces don’t need it, and the test suites (npm test,
npm run test:unit) run without any AI configuration at all.
A feature or fix is not done until all of these pass. State plainly what you ran and what passed — if something is unverified, say so.
npm run typecheck — TypeScript strict, no errors.
npm test boots the real renderer against fixtures and asserts no page 500s.
At the right layer — unit, smoke, or e2e. See testing.
Screenshot the affected page (agent-browser, light and dark mode if visual).
Don’t claim a UI change works from the DOM alone — layout bugs (e.g. the Card h-full
height bug) only show visually.
node tests/crawl.mjs <cloned-docs-repo> — expect 0 × HTTP 500.
Regression protection is a hard requirement, not optional. If you added a pure helper, unit-test it. If you fixed a render bug, add a fixture. See testing for which layer fits.
When a change needs hands-on verification (DoD step 4, or someone says “go test this”), don’t hand-walk signup → onboarding. Seed a known account and drive a real browser.
docker compose up -d # Postgres (+pgvector) + MinIO (S3)
npm run db:seed
scripts/seed-dev.mjs creates two logins, both password password —
dev@papervine.local (owner) and dev2@papervine.local (admin) — as members of
the same org, plus a connected site starter (→ a real-world docs repo) with activity +
analytics data. The second account is there so you can log in as both (two browser
profiles) and exercise real-time collab. It is idempotent (re-run to refresh the
passwords) and prod-guarded — it refuses any non-localhost DATABASE_URL.
The control plane lives on the app. host. Log in at
http://app.localhost:3000/login; the dashboard is at bare
(seed → ). New
site at .
The apex (localhost:3000) is marketing + docs and bounces auth paths to the app host.
Drive everything with agent-browser (the repo standard — open / snapshot -i /
click @eN / fill / screenshot; it navigates app.localhost and {slug}.localhost
fine) or Playwright. This is the same loop that catches per-request content-source bugs:
connect a repo, open its docs, confirm the sidebar and pages are the tenant’s, not the
platform’s.
The schema is versioned — every change is a committed SQL migration, never a live
push. Pushing the migration is shipping it.
src/lib/db/schema.ts is Better Auth (regeneratable); src/lib/db/app-schema.ts is
our tables.
npm run db:generate
Writes drizzle/NNNN_*.sql (+ meta/). Commit it and review the SQL like any code.
npm run db:migrate
CI’s e2e rebuilds papervine_test from these same files, so a broken migration fails CI.
vercel.json’s build command runs drizzle-kit migrate before next build, so each
Vercel preview migrates its own Neon branch. No manual prod steps, no .
Destructive changes (drop / rename) need care: generate, read the SQL, and prefer
expand-then-contract. Drizzle’s journal lives in a separate drizzle schema — a full
local reset is DROP SCHEMA public CASCADE; DROP SCHEMA drizzle CASCADE; CREATE SCHEMA public; then migrate.
docker compose up -d # local Postgres (+pgvector) + MinIO (S3) — `npm run dev` does this for you
npm run dev # the whole stack: docker services, the app, and the automations
# worker (cron ticks and runs execute only while it's connected).
# Peripheral layers attach when you've configured them — Stripe
# webhook forwarding starts only if STRIPE_SECRET_KEY,
# STRIPE_WEBHOOK_SECRET, and the `stripe` CLI are all present.
# Every layer degrades independently: no docker → no-DB mode, no
# Trigger.dev login → automations just don't execute.
npm run dev:app # just the Next server (renderer-only work)
npm run dev:fresh # kill the dev server, wipe .next, restart clean (use when chunks/manifests are corrupted)
npm run build # production build
npm run typecheck # tsc --noEmit
"use client"
only for interactivity (see src/components/mdx/Tabs.tsx, Accordion.tsx).SPEC.md and GAP-REPORT.md current when you make an architectural decision or
change what renders — record the decision and the measured result.Co-Authored-By
trailer already used in this repo’s history.Two principles guide every renderer change: never let one unsupported feature 500 a
page (unknown components degrade to their children; compile failures render an inline
notice), and config is a compatibility layer — warn, don’t throw. docs.json is
compatible with existing docs.json projects, so real repos migrate unchanged. When unsure
how something should behave, check against a real-world docs.json repo rather than guess.
npm run test:e2e (needs docker Postgres + MinIO up).
app.localhost:3000/:org/:siteapp.localhost:3000/dev-org/starter/:org/connectAny dev port works: when :3000 is busy (several worktrees running at once), Next
auto-picks :3001/:3002… and auth follows — development trusts localhost,
*.localhost, and 127.0.0.1 on any port (trustedOrigins in src/lib/auth.ts),
so sign-in works without touching BETTER_AUTH_URL. Production never trusts these
wildcards.
Tenant docs render at {slug}.localhost:3000 (subdomain mode) or /sites/{slug}
(apex path mode). resolveTenantSlug picks the mode off the Host header; isAppHost
picks the control plane.
push --force