A self-hostable business directory and storefront platform. Every business gets a storefront it can build without a developer; every user finds them through search, a map, and published rankings.
Everything below runs against the two seeded Jakarta businesses, no signup
needed. To click through as a human, sign in as the demo owner
(demo@allstuff.test / DemoPass123!) or the platform admin
(admin@allstuff.test / DemoPass123!, 2FA-enrolled — see Run it).
Find a business. Start at the landing page, then search with filters, suggestions, and sorting — or browse by category, city, or the map. Compare businesses side by side.
| Landing (live verified-business count) |
|---|
![]() |
| Discover search (2 seeded results, live) |
|---|
![]() |
Visit a storefront. Catalog with prices, opening hours, location, reviews, helpful votes, Q&A, and trust badges — Verified means an admin checked the business; Fully Verified means they also read its registration documents.
| Storefront (trust badges, catalog, hours) |
|---|
![]() |
Book a visit, or just message them. Services show real availability derived from hours, staff shifts, and blackouts; booking is free. Chat threads support attachments, replies, and push notifications.
See what is trending. Engagement decays with time, so rankings reflect what is hot now, not what has always been big. A separate Rising board reserves visibility for new and small businesses.
| Trending leaderboards |
|---|
![]() |
Run a business. The owner dashboard manages the storefront, catalog, bookings with day/week views and a waitlist, staff and locations, reviews and replies, announcements, and billing. Sign in as the demo owner — both seed businesses are already there.
| Owner dashboard (real login, both seed businesses) |
|---|
![]() |
Run the platform. Admins verify businesses against submitted documents (every view is audit-logged), moderate reports and appeals, curate featured placements, and watch KPIs. Sign in as the admin (scan the printed QR into an authenticator app — the admin gate requires the enrolled 2FA factor).
Captured against the seed by apps/web/scripts/shots.mjs (Playwright +
go run ./cmd/api -dev-login); re-run it after visual changes so the tour
cannot rot.
PostgreSQL, Redis, and one Go binary. Three direct Go dependencies — JWT, TOTP, Argon2id, the Redis client, Stripe, Web Push and the Prometheus exposition are all hand-rolled. No per-seat pricing, no data resale. (Stripe for owner subscriptions, Google OAuth, and Resend/SMTP for email are the proprietary edges; everything else is Postgres, Redis, and local disk.)
apps/web/ Next.js 16 + React 19 + TypeScript + Tailwind (light/dark, en/id)
services/api/ Go 1.27 — REST + WebSocket, 258 routes, 28 services
infra/ docker compose, 48 versioned SQL migrations
docs/ PRD (frozen), API (authoritative), ARCHITECTURE, RUNBOOK, DEPLOYMENT
1. Trust is document-backed, and every view of a document is audited.
A business submits its registration or licence; an admin opens it in a viewer
that serves with CSP: default-src 'none'; sandbox, and each open writes a
verification_document_views row. Documents are AES-256-GCM encrypted at rest
and are never publicly reachable. There are two levels, and a paid plan cannot
buy the higher one — see claim 3.
grep -rn "LogDocumentView" services/api/internal/ # the audit write
grep -rn "document_verification" infra/postgres/migrations/0001_initial.sql2. The ranking formula is published, and it has a fairness floor.
score = Σ(weight × e^(−λ·age_hours)) over three windows, with the weights
documented (view 1, save 3, like 5, comment 8, chat 8, recommend 10, review 12)
and λ admin-tunable. There is a Rising tier — velocity normalised by business
age and trailing score — that holds guaranteed visibility slots for new and small
businesses, plus a tracked KPI for the share of those slots they actually win.
Helpful votes feed no score at all, by design.
Every platform that ranks businesses monetises incumbent rank, which is why none of them can ship this.
sed -n '1,80p' infra/postgres/migrations/0030_open_now.sql # the SQL twin
grep -n "RisingPaused\|lambda\|RisingN" services/api/internal/service/trending.go3. Paid tiers sell capacity. They never sell trust or placement. A plan can charge for how many products, how many gallery photos, how many team seats, API access, analytics depth, and support priority. It cannot charge for the verification badge, because an admin assigns that after reading documents, and it cannot charge for a featured slot, because featured is an editorial choice. Those two entitlements were in the original catalogue and were removed; a test now fails if anyone puts them back.
grep -n "verified_badge\|featured_placement" services/api/internal/service/billing_test.goRequires Go 1.27, Node 24, and Docker (or a local PostgreSQL 17).
# 1. backing services (redis shares the rate limiter, the TOTP replay guard
# and WS fan-out across replicas; without it everything degrades to
# per-instance. mailpit catches dev email.)
docker compose -f infra/docker/docker-compose.yml up -d postgres redis mailpit
# 2. database + migrations (the Go runner is the only supported path)
cd services/api
export DATABASE_URL="postgres://postgres:postgres@localhost:5432/allstuff?sslmode=disable"
export JWT_SECRET="$(openssl rand -hex 32)"
go run ./cmd/api -migrate
# 3. API on :8080
go run ./cmd/api
# 4. web on :3000. Local dev proxies /api to :8080 via next.config.ts
# (API_ORIGIN, default http://localhost:8080) and dials the API directly for
# WS (NEXT_PUBLIC_WS_BASE=ws://localhost:8080). In prod the vercel.json rules
# proxy both same-origin.
cd ../../apps/web && npm install && npm run devLoad scripts/seed.sql for demo data — two verified Jakarta businesses, a
category tree, engagement signals (so trending/leaderboards render), and two
users: demo@allstuff.test (owner of both businesses) and
admin@allstuff.test (platform admin). The seed hashes are placeholders, not
logins — applying a seed file must never create credentials on its own:
# From the repository root (the previous revision showed this from
# services/api, where the relative path does not resolve):
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f scripts/seed.sqlTo click through as a human, turn those rows into working logins (dev only —
the flag refuses APP_ENV=prod, and the passwords are documented because a
dev password you have to look up gets replaced with something weaker):
go run ./cmd/api -dev-login
# owner demo@allstuff.test / DemoPass123!
# admin admin@allstuff.test / DemoPass123! (scan the printed QR into an authenticator app;
# the admin gate requires an enrolled 2FA factor)Manual equivalents, if you prefer SQL: UPDATE users SET email_verified_at = now() WHERE email = '...' skips verification for a new registration, and
UPDATE users SET role = 'admin' WHERE email = '...' promotes one (then
enroll 2FA at /me/security before /admin/* will open).
There is no dotenv loader.
config.Load()readsos.Getenvonly, so copying.env.exampleto.envand running the binary silently uses the built-in defaults. Export the variables, as above. An earlier version of this README told you tocopy .env.example .env, which did nothing.
Three flags exist: -migrate (apply migrations and exit), -vapid (print a
Web Push keypair and exit), and -dev-login (turn the seed users into working
dev logins; refuses APP_ENV=prod). Everything else is environment.
cd services/api
go vet ./...
go test -race ./... # required: several tests are concurrency tests
go test -cover ./...
# Migration safety — needs a PostgreSQL you may create and drop databases on
TEST_DATABASE_URL="postgres://postgres@localhost:5432/postgres?sslmode=disable" \
go test ./internal/db/ -timeout 30m
cd ../../apps/web
npm run typecheck && npm run lint
npx playwright install chromium && npx playwright testgo test ./... covers the API wall to wall (the ratchet above fails the build
on any drop). Playwright covers the public and auth journeys end to end —
landing, discover, booking as a customer, chat, notifications — plus axe
accessibility across the surfaces. The owner dashboard happy path, admin, and
billing flows need a verified email, an enrolled 2FA factor, or real Stripe
keys, so those are covered by Go tests, not by the browser suite.
| File | What it is |
|---|---|
docs/API.md |
The endpoint reference. TestEveryAPIRouteIsDocumented fails when a route group is absent, so no area of the API goes undocumented — the four /admin/categories endpoints and GET /auth/csrf were, until it was written. It checks groups rather than individual endpoints, deliberately: the document's read|typing|close family notation cannot be compared path-by-path, and a stricter version reported 31 false positives for the 2 real findings. |
docs/ARCHITECTURE.md |
Design decisions, invariants, and the traps. Not an endpoint list. |
docs/RUNBOOK.md |
Admin operations, verification standards, incident response, migration recovery. |
docs/DEPLOYMENT.md |
Vercel + Railway, with every env var that fails startup. |
SECURITY.md |
Data handled, known limitations, and the doc-drift policy. |
CONTRIBUTING.md |
The migration contract, enforced. |
docs/PRD.md |
Frozen 2026-08 product contract. The shipped code has outgrown it — do not use it as an acceptance criterion. |
infra/postgres/migrations/REGISTRY.md
is a registry of reserved migration numbers and an inventory of latent defects
in this repository's own schema history — a silent IF NOT EXISTS name
collision that meant an index was never built, two byte-identical duplicate
indexes on the highest-write table, a seed that could wedge startup permanently,
and a NOT NULL without a DEFAULT that 500'd claim approval.
It also records what the migration replay test found: re-running 0015
recreates three indexes 0031 deliberately dropped, including a duplicate on
notifications, and nothing reports an error. The reasoning behind every
one of these is written down, because the next person to touch a migration needs
to know why the rule exists.
Named here rather than discovered later:
- Row-level security is a read backstop, not the authorization layer.
The request path connects as the least-privilege
allstuff_approle (migrations0041/0053) and every row on tenant tables is re-checked against the requesting user. Jobs, migrations and the notifier run as the table owner and bypass RLS by design. SeeSECURITY.md. - Multi-location businesses work; mobile/service-area ones do not.
Locations with per-place hours exist (
0043,0050), butlat/lngare stillNOT NULL, so a business with no fixed address is unrepresentable. Migration0036is reserved and is the smallest high-value fix left in the schema. - English-first UI. i18next is wired end to end and
idis complete forcommon/nav/landing/auth, but most feature surfaces are still hardcoded English. The switcher changes what it covers, not everything. - No FFmpeg pipeline. Images get one ≤512px JPEG thumbnail; video and audio are stored and served as uploaded.
- No object storage. Media is local disk; a single replica with a volume is the supported production topology.
- Google OAuth only, despite the PRD listing four providers.
- No recurring bookings or payment at booking time. Booking itself is built (availability derived from hours × staff × blackouts, blackout management, multi-location rosters, confirm/complete/no-show) and booking is free; paid plans are owner subscriptions via Stripe, not per-appointment charges.
MIT. See LICENSE.




