Imported from dkarzon/buy-a-bit (
AGENTS.md). Install upstream withnpx skills add dkarzon/buy-a-bit. Copyright stays with the author.
AGENTS.md
Guidance for AI coding agents working in this repo. Humans: see README.md. Product/architecture detail: docs/idea1-buy-a-bit.md and docs/idea1-buy-a-bit-tech-stack.md.
What this is
buy-a-bit — Pinch Hackathon app: NFC/QR → product landing → custom CaptureJS payment page → realtime charge → confirmation. Merchants manage products; customers check out as guests or optionally sign in for order history and one saved card per merchant.
pnpm monorepo, Node ≥ 20:
| Package | Path | Role |
|---|---|---|
api |
apps/api |
Hono + tRPC + Drizzle + Pinch webhooks (port 3001) |
web |
apps/web |
Vite + React 19 + Tailwind 4 + tRPC client (port 5173) |
@buy-a-bit/shared |
packages/shared |
Shared Zod schemas, procedure I/O, constants |
Scaffold is Phase 0: routers/pages exist; many procedures throw Not implemented. Prefer implementing stubs over inventing parallel APIs.
Setup & commands
pnpm install
cp .env.example .env && cp .env.example apps/api/.env && cp .env.example apps/web/.env
docker compose up -d # local Postgres
pnpm dev # API + web
pnpm typecheck # all packages — run before finishing a task
pnpm db:push # Drizzle push (hackathon / local only)
pnpm db:generate && pnpm db:migrate # real migrations before shared/deployed DB
- Filter:
pnpm --filter api <script>/pnpm --filter web <script>/pnpm --filter @buy-a-bit/shared <script> - Health:
http://localhost:3001/health - No test suite yet — use
pnpm typecheckas the default verification gate. Add tests when introducing non-trivial logic.
Boundaries
Always
- Read
@buy-a-bit/shared(schemas.ts,procedures.ts) before changing tRPC inputs/outputs; update shared Zod first, then API + web. - Store money as integer cents (
priceCents), never floats. - Keep Pinch HTTP in
apps/api/src/services/; routers orchestrate, services call Pinch. - Resolve Pinch via
pinchClientForMerchant(merchant): managed = platform Application credentials +Current-Merchant: mch_…; BYOK = merchant’s encrypted Application ID/Secret (no impersonation header). See Managed Merchants anddocs/idea1-buy-a-bit.md. - Treat webhooks as reconciliation for payment status; realtime API response drives immediate UX on the custom payment page.
- Use
credentials: "include"on the web tRPC client; CORS must allowWEB_URLwith credentials. - Prefer existing patterns: ESM
.jsimport suffixes in API,publicProcedure/protectedProcedure, pages underapps/web/src/pages/. - Run
pnpm typecheckafter substantive TS changes and fix errors you introduced.
Ask first
- New top-level apps/packages, new payment providers, or schema redesigns that break existing contracts.
- Adding a UI component library (e.g. shadcn) or switching away from Vite SPA.
- Committing, pushing, or opening PRs — only when the user asks.
- Stretch features (
/admin/*,/s/:storeSlug, variants) unless the task clearly targets them.
Never
- Commit
.env, secrets, or Pinch credentials. Use.env.examplefor new keys only. - Call Pinch (or any secrets) from the browser.
- Trust client-supplied price/merchantId on checkout — load product server-side and authorize by
ctx.merchant. - Skip webhook signature verification when implementing
/webhooks/pinch. - Invent REST endpoints for app features — extend tRPC routers instead (auth + Pinch webhook are the intentional non-tRPC routes).
- Rewrite working scaffold for style-only reasons.
Architecture map
apps/api/src/
index.ts Hono: CORS, /health, /trpc/*, /webhooks/pinch (+ /api/auth/* Day 1)
auth.ts Better Auth (Day 1)
db/ Drizzle client + schema (auth tables + merchants/products/orders/order_items)
trpc/ context, middleware, routers: merchant | product | order | payment
services/ pinch.ts, qr.ts
webhooks/pinch.ts raw body + signature verify
apps/web/src/
pages/ routes wired in App.tsx
components/ e.g. ProtectedRoute
lib/trpc.ts createTRPCReact<AppRouter> + credentials
lib/auth-client.ts better-auth/react (Day 1)
packages/shared/ contract of record for procedure I/O
Key flows
- Merchant: sign-in → onboarding (
merchant.createwithmanagedorbyok) → product CRUD → QR/landing URL. - Customer:
/p/:slug→order.create→/pay/:orderId(CaptureJS) →payment.charge→/payment/complete?session={orderId}. - Async: Pinch webhook updates
orders.status(pending|paid|failed); managed may also receivecompliance-updated.
Auth
- One Better Auth
usertable for both roles: any signed-in user is a customer; a merchant additionally has amerchantsrow (merchants.userId). protectedProcedure= session only (customer-grade);merchantProcedure= session + linked merchant. Public: landing, checkout, payment verify.- Customer sign-in is
/account/login; merchant sign-in stays/login. Guest checkout must keep working —orders.userIdis nullable. - Stored cards:
customer_payerskeys a Pinch payer by(userId, merchantId); one vaulted source per row. Saved-card charges (useSavedCard) require the session user to own the order — an order id alone must never charge a card on file. - Import auth hooks from
better-auth/react, notbetter-auth/client.
Conventions
- Contract-first: change
@buy-a-bit/sharedwhen procedure shapes change; keep API.input()wired to those schemas. - Types: export
AppRouterfrom API; web importsapi/routerfor client typing. - IDs: UUIDs in shared schemas; product public URLs use slug (
/p/:slug), not raw UUID. - Return URL / confirmation:
{WEB_URL}/payment/complete?session={orderId}after custom-page charge (not a Pinch hosted return URL). - Pinch metadata on realtime payments:
orderId,merchantId,customerName,customerEmail. - Payments: CaptureJS client tokenisation +
POST /payments/realtime— do not use Pinch Payment Links for MVP. - DB: one Drizzle schema file for auth + app tables; generate Better Auth tables via
@better-auth/cli generatethen merge. - UI: Tailwind utility styling; no required component library for MVP. Match existing page structure.
- Scope: implement MVP paths in the tech-stack doc before stretch admin/store catalog unless asked.
Do / Don't
| Do | Don't |
|---|---|
| Fill in existing router stubs and page shells | Create duplicate routers/pages beside them |
Put shared Zod in packages/shared |
Duplicate Zod objects in API and web |
Authorize mutations with product.merchantId === ctx.merchant.id |
Trust that the client owns the product id |
Use db:push for solo local iteration |
Use push against a shared/prod database |
Point agents at docs/ for deep design |
Paste the whole tech-stack doc into chat/code |
Security checklist (payments & auth)
- Webhook verifies signature (
PINCH_WEBHOOK_SECRETand/or per-BYOK secret) before mutating orders - Checkout loads product/price from DB; ignores client price; Pinch client resolved from order’s merchant
- Card PAN/CVC never touch the API — only CaptureJS
creditCardToken - Managed calls always send
Current-Merchantwhen acting for a sub-merchant - BYOK secrets encrypted at rest; never returned to the client or logged; publishable key ok for browser
- Protected mutations scoped to the session merchant
- Secrets only in server env; never
VITE_-prefixed for Application secrets - CORS origin is explicit (
WEB_URL), credentials enabled deliberately
When stuck
- Check whether the procedure I/O already exists in
packages/shared/src/procedures.ts. - Skim the relevant section of
docs/idea1-buy-a-bit-tech-stack.md(routers, schema, Pinch, auth). - Mirror the nearest working stub (e.g.
productRouter) rather than introducing a new pattern.
Update this file when agents repeatedly make the same mistake, or when setup/commands change.