|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +Guidance for AI coding agents working in the Devix repository. Primary branch: `main`. This file is authoritative over the stale root `README.md`. |
| 4 | + |
| 5 | +For architecture, data flows, trade-offs, and technical debt, see [`DESIGN.md`](DESIGN.md). |
| 6 | + |
| 7 | +## Project Summary |
| 8 | + |
| 9 | +Devix is a premium software-agency platform: marketing site, portfolio, project estimator, digital product store, and admin CMS/dashboard. Single Next.js app (not a monorepo), managed with Bun, deployed on Vercel (region `sin1`). |
| 10 | + |
| 11 | +## Stack & Tooling |
| 12 | + |
| 13 | +- **Framework:** Next.js 16 App Router, React 19, TypeScript 6, React Compiler, typed routes/env |
| 14 | +- **Database:** Prisma 7 + `@prisma/adapter-pg` → Supabase PostgreSQL (pooler for runtime, direct for migrations) |
| 15 | +- **Storage:** Supabase Storage (product files, media library) |
| 16 | +- **Cache / rate limits:** Upstash Redis (optional in dev — graceful fallback when unset) |
| 17 | +- **Email:** Resend |
| 18 | +- **Payments:** Stripe and Lemon Squeezy behind a provider abstraction |
| 19 | +- **Auth:** Custom admin sessions (bcrypt + JWE cookies) — **not** Supabase Auth or NextAuth |
| 20 | +- **Testing:** Vitest (unit), Playwright (e2e visual baselines — not in CI) |
| 21 | +- **Lint/format:** ESLint flat config, Prettier with Tailwind plugin |
| 22 | + |
| 23 | +Use **Bun** for all scripts (`bun dev`, `bun test`, etc.). Do not assume npm/pnpm. |
| 24 | + |
| 25 | +| Command | Purpose | |
| 26 | +|---------|---------| |
| 27 | +| `bun dev` | Prisma generate + Next dev server | |
| 28 | +| `bun build` / `bun start` | Production build and serve | |
| 29 | +| `bun lint` / `bun type-check` / `bun test` | CI-equivalent checks | |
| 30 | +| `bun db:seed` | Seed CMS, portfolio, sample products | |
| 31 | + |
| 32 | +## Repository Layout |
| 33 | + |
| 34 | +``` |
| 35 | +src/ |
| 36 | +├── app/ # Routes: (public)/, admin/(dashboard)/, api/webhooks/ |
| 37 | +├── actions/ # Server actions (public + admin/) |
| 38 | +├── lib/ # Business logic, payment, queries, schemas, redis |
| 39 | +├── components/ # atoms/, molecules/, organisms/, admin/ |
| 40 | +├── proxy.ts # Request proxy (rate limits + admin auth gate) |
| 41 | +├── hooks/, types/, utils/, content/ |
| 42 | +prisma/ # schema.prisma, migrations/, seed.ts |
| 43 | +e2e/ # Playwright tests |
| 44 | +``` |
| 45 | + |
| 46 | +**Layering:** `app/` pages → `actions/` (mutations) or `lib/queries/` (reads) → `lib/` → Prisma / Supabase / Redis / payment SDKs. |
| 47 | + |
| 48 | +## Local Setup & Required Env |
| 49 | + |
| 50 | +1. Copy [`.env.example`](.env.example) → `.env.local` |
| 51 | +2. Fill Supabase Postgres URLs (`DATABASE_URL` pooler port 6543, `DIRECT_URL` direct port 5432) |
| 52 | +3. Run migrations: `bunx prisma migrate deploy` (uses `DIRECT_URL` via [`prisma.config.ts`](prisma.config.ts)) |
| 53 | + |
| 54 | +**Build fails without:** `DATABASE_URL`, `JWT_SECRET`, `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY` (enforced in [`next.config.ts`](next.config.ts)). |
| 55 | + |
| 56 | +See [`.env.example`](.env.example) for Stripe, Lemon Squeezy, Resend, Turnstile, Redis, and payment provider vars. Do not duplicate the full list here. |
| 57 | + |
| 58 | +## Architecture Overview |
| 59 | + |
| 60 | +```mermaid |
| 61 | +flowchart LR |
| 62 | + subgraph public [PublicSite] |
| 63 | + Pages[AppRouterPages] |
| 64 | + Actions[ServerActions] |
| 65 | + Queries[lib/queries] |
| 66 | + end |
| 67 | + subgraph admin [AdminDashboard] |
| 68 | + AdminActions[admin/actions] |
| 69 | + Proxy[proxy.ts] |
| 70 | + end |
| 71 | + subgraph infra [Infrastructure] |
| 72 | + Prisma[(PostgreSQL)] |
| 73 | + Storage[SupabaseStorage] |
| 74 | + Redis[UpstashRedis] |
| 75 | + Pay[StripeOrLemonSqueezy] |
| 76 | + end |
| 77 | + Pages --> Queries --> Prisma |
| 78 | + Pages --> Actions --> Prisma |
| 79 | + Actions --> Redis |
| 80 | + AdminActions --> Proxy |
| 81 | + AdminActions --> Prisma |
| 82 | + AdminActions --> Storage |
| 83 | + StoreCheckout[StoreCheckout] --> Pay |
| 84 | + Pay --> Webhooks[api/webhooks] |
| 85 | + Webhooks --> Fulfillment[purchase-fulfillment] |
| 86 | + Fulfillment --> Prisma |
| 87 | + Fulfillment --> Email[Resend] |
| 88 | + DownloadRoute["/download/token"] --> Storage |
| 89 | +``` |
| 90 | + |
| 91 | +Three domains to respect: |
| 92 | + |
| 93 | +1. **Public site** — cached Prisma reads, form actions (contact, estimator, checkout) |
| 94 | +2. **Admin CMS** — authenticated server actions, Supabase uploads, cache invalidation |
| 95 | +3. **Store** — checkout starts payment; **webhooks only** create `Purchase` rows and download tokens |
| 96 | + |
| 97 | +## Request Proxy, Auth & Security |
| 98 | + |
| 99 | +**Proxy:** [`src/proxy.ts`](src/proxy.ts) is the Next.js 16 request proxy (not `middleware.ts`). It applies global and `/api/` rate limits and gates `/admin/*` routes via session cookies. |
| 100 | + |
| 101 | +**Admin auth (custom):** |
| 102 | + |
| 103 | +- Users in Prisma `User` model; passwords hashed with bcrypt |
| 104 | +- Sessions: JWE-encrypted HTTP-only cookies via `jose` ([`src/lib/auth.ts`](src/lib/auth.ts), [`src/lib/session-token.ts`](src/lib/session-token.ts)) |
| 105 | +- Cookie names: `__Host-devix_session` (HTTPS prod), `devix_session` (dev) |
| 106 | +- Defense in depth: proxy → dashboard layout `verifyAdminSession()` → per-action auth + `verifyCsrfOrigin()` |
| 107 | +- Flat admin model: max 4 users, `isActive` flag; public `/admin/register` is disabled (invite-only) |
| 108 | + |
| 109 | +**Other security:** Production CSP and security headers in `next.config.ts`; Cloudflare Turnstile on checkout (optional); Bearer token on [`src/pages/api/health.ts`](src/pages/api/health.ts). |
| 110 | + |
| 111 | +Do not introduce Supabase Auth, NextAuth, or RBAC unless explicitly requested. |
| 112 | + |
| 113 | +## Supabase Usage (Postgres + Storage Only) |
| 114 | + |
| 115 | +- **Queries:** Prisma → Postgres (no Supabase JS for DB access) |
| 116 | +- **No RLS** — authorization is application-layer only |
| 117 | +- **Browser client:** [`src/lib/supabase-browser.ts`](src/lib/supabase-browser.ts) — admin media uploads |
| 118 | +- **Service client:** [`src/lib/supabase-admin.ts`](src/lib/supabase-admin.ts) — signed download URLs, product bucket `products` |
| 119 | +- [`src/lib/supabase-server.ts`](src/lib/supabase-server.ts) exists but is unused — do not wire new code through it without reason |
| 120 | + |
| 121 | +Requires `SUPABASE_SERVICE_ROLE_KEY` for downloads and admin storage operations (see `.env.example`). |
| 122 | + |
| 123 | +## Money & Pricing (Decimal) |
| 124 | + |
| 125 | +All persisted money uses Prisma **`Decimal`**, not `Float` or `Int`. Helpers: [`src/lib/money.ts`](src/lib/money.ts). |
| 126 | + |
| 127 | +| Field | Semantics | |
| 128 | +|-------|-----------| |
| 129 | +| `Product.price` | Major units (`Decimal(19,4)`) — admin input / display | |
| 130 | +| `Product.priceMinor` | Smallest currency unit (`Decimal(19,0)`) — canonical charge amount | |
| 131 | +| `Purchase.amountMinor` | Paid snapshot at fulfillment | |
| 132 | +| `EstimatorLead.budgetUsd` / `deliverableSavingsUsd` | USD quote amounts (`Decimal(19,2)`) | |
| 133 | + |
| 134 | +**Rules for agents:** |
| 135 | + |
| 136 | +- Use `resolveProductAmount()` before checkout; prefer `priceMinor` when set |
| 137 | +- Use `formatMinor()` / `formatUsdDecimal()` for display — not raw `.toFixed()` on floats |
| 138 | +- Convert to Stripe/Lemon `number` only at SDK boundary via `minorToStripeUnit()` / `stripeUnitToMinor()` |
| 139 | +- Import `Decimal` from `@/lib/money` (re-exported from Prisma runtime) |
| 140 | +- Do not pass raw `Decimal` to client components — serialize to string in props |
| 141 | + |
| 142 | +See [DESIGN.md §7](DESIGN.md#7-money--pricing-model) for full model and migration notes. |
| 143 | + |
| 144 | +## Payments (Dual Provider Abstraction) |
| 145 | + |
| 146 | +- Interface: [`src/lib/payment/types.ts`](src/lib/payment/types.ts) — `PaymentProvider` |
| 147 | +- Factory: [`src/lib/payment/index.ts`](src/lib/payment/index.ts) — `getPaymentProvider()` |
| 148 | +- Active provider resolution ([`src/lib/payment/config.ts`](src/lib/payment/config.ts)): Admin `SiteSettings.paymentProvider` → `PAYMENT_PROVIDER` env → `"stripe"` |
| 149 | +- Stripe: embedded checkout on `/store/checkout`; Lemon Squeezy: overlay checkout, requires `Product.lemonSqueezyVariantId` |
| 150 | + |
| 151 | +**Critical:** Fulfillment is **webhook-only**. Checkout actions in [`src/actions/purchase.ts`](src/actions/purchase.ts) never create `Purchase` records. |
| 152 | + |
| 153 | +- Webhooks: [`src/app/api/webhooks/stripe/route.ts`](src/app/api/webhooks/stripe/route.ts), [`src/app/api/webhooks/lemonsqueezy/route.ts`](src/app/api/webhooks/lemonsqueezy/route.ts) |
| 154 | +- Idempotency: `ProcessedPaymentEvent` table |
| 155 | +- Fulfillment: [`src/lib/purchase-fulfillment.ts`](src/lib/purchase-fulfillment.ts) |
| 156 | +- Revocation (refunds/disputes): [`src/lib/purchase-revocation.ts`](src/lib/purchase-revocation.ts) |
| 157 | + |
| 158 | +When editing Stripe webhooks, follow the existing inline pattern in that route (it partially bypasses the provider abstraction). Lemon webhooks use the abstraction consistently. |
| 159 | + |
| 160 | +## Digital Store & Download Tokens |
| 161 | + |
| 162 | +**Models:** `Product`, `Purchase` in [`prisma/schema.prisma`](prisma/schema.prisma). |
| 163 | + |
| 164 | +**Checkout flow:** User pays → provider webhook → fulfillment creates `Purchase` with `downloadToken` → confirmation email links to `/download/{token}`. |
| 165 | + |
| 166 | +**Download limits (not strictly one-time):** |
| 167 | + |
| 168 | +- Default **max 3 downloads** within a **24-hour** window from fulfillment |
| 169 | +- `tokenUsed` set when `downloadCount >= maxDownloads` |
| 170 | +- `revokedAt` blocks access (refunds, disputes, admin revoke) |
| 171 | +- `deliveredFileKey` snapshots the product file at purchase time |
| 172 | +- Each download request gets a signed Supabase URL (300s TTL) via [`src/lib/download-token.ts`](src/lib/download-token.ts) |
| 173 | + |
| 174 | +Status pages: `/download/used`, `/expired`, `/revoked`, `/invalid`, `/error`. Admin can rotate tokens or resend email via [`src/actions/admin/purchases.ts`](src/actions/admin/purchases.ts). |
| 175 | + |
| 176 | +## Data Layer (Prisma) |
| 177 | + |
| 178 | +- `relationMode = "prisma"` — Supabase-friendly; no DB-level FK enforcement |
| 179 | +- Client singleton: [`src/lib/prisma.ts`](src/lib/prisma.ts) with `pg` Pool adapter |
| 180 | +- Migrations in `prisma/migrations/`; seed with `bun db:seed` |
| 181 | + |
| 182 | +**Model groups:** CMS (`SiteSection`, `FaqItem`, `ServiceItem`, `TeamMember`, `SiteSettings`), portfolio (`Project`), CRM (`ContactSubmission`, `EstimatorLead`, `ConsultationFeedback`), commerce (`Product`, `Purchase`, `ProcessedPaymentEvent`), audit (`ActivityLog`, `MediaAsset`). |
| 183 | + |
| 184 | +## Caching & Rate Limiting |
| 185 | + |
| 186 | +[`src/lib/redis.ts`](src/lib/redis.ts): `cachedQuery()` for read-through cache, `invalidateCache()` on admin writes. Returns `null` when Upstash env is missing — code must not assume Redis is available. |
| 187 | + |
| 188 | +Rate limiters in [`src/lib/rate-limit.ts`](src/lib/rate-limit.ts) cover auth, contact, purchase, download, API, etc. When adding admin mutations that change cached content, follow existing actions and call `invalidateCache()` with the same keys. |
| 189 | + |
| 190 | +## Server Actions vs API Routes |
| 191 | + |
| 192 | +**Default to server actions** (`"use server"` in [`src/actions/`](src/actions/)) for all UI mutations. |
| 193 | + |
| 194 | +**API routes only for:** |
| 195 | + |
| 196 | +- Payment webhooks (raw body + signature verification) |
| 197 | +- Admin CSV export ([`src/app/api/admin/leads/export/route.ts`](src/app/api/admin/leads/export/route.ts)) |
| 198 | +- Legacy health check ([`src/pages/api/health.ts`](src/pages/api/health.ts) — Pages Router) |
| 199 | + |
| 200 | +**Page data:** use [`src/lib/queries/`](src/lib/queries/), not server actions. |
| 201 | + |
| 202 | +**Error patterns:** |
| 203 | + |
| 204 | +- Public actions: return `{ success/ok, error? }` — do not throw |
| 205 | +- Admin actions: often `throw new Error(...)` after Zod/auth failures |
| 206 | +- Validation: Zod schemas in [`src/lib/schemas.ts`](src/lib/schemas.ts); use `safeParse` + first issue message |
| 207 | + |
| 208 | +Server action body limit: 50mb (large admin media uploads). |
| 209 | + |
| 210 | +## Code Conventions |
| 211 | + |
| 212 | +- Imports: `@/` alias only; keep imports at top of file (no inline imports) |
| 213 | +- Semicolons, double quotes, 2-space indent (Prettier) |
| 214 | +- Components: PascalCase (`StoreProductCard.tsx`); lib modules: kebab-case (`purchase-fulfillment.ts`) |
| 215 | +- Tests: sibling `test/` folders, `*.test.ts`; mock at module boundaries (`@/lib/prisma`, not `@prisma/client`) |
| 216 | +- See [`src/lib/test/mocks/README.md`](src/lib/test/mocks/README.md) for mock kit conventions |
| 217 | +- `no-console` enforced in production builds |
| 218 | +- Use exhaustive `never` checks in switch defaults over discriminated unions |
| 219 | +- Minimize scope; match surrounding code style |
| 220 | + |
| 221 | +## Domain Features (Pointers) |
| 222 | + |
| 223 | +- **Project estimator:** [`src/lib/estimator-*.ts`](src/lib/estimator-steps.ts), leads via [`src/actions/estimator-leads.ts`](src/actions/estimator-leads.ts) |
| 224 | +- **CMS:** JSON blobs in `SiteSection` plus relational FAQ/services/team tables |
| 225 | +- **Maintenance mode:** `SiteSettings.maintenanceMode` + [`MaintenanceGate`](src/components/MaintenanceGate.tsx) |
| 226 | +- **Media library:** Supabase Storage + `MediaAsset` metadata in Prisma |
| 227 | + |
| 228 | +## Testing & CI |
| 229 | + |
| 230 | +- **Unit:** Vitest; run `bun test`. Colocated tests with shared mock registry in `src/lib/test/mocks/` |
| 231 | +- **E2E:** Playwright in `e2e/` — not run in main CI workflow |
| 232 | +- **CI** ([`.github/workflows/ci.yaml`](.github/workflows/ci.yaml) on `main` and `dev`): lint → type-check → test → build |
| 233 | + |
| 234 | +**Before claiming work is done:** run `bun lint`, `bun type-check`, and `bun test`. |
| 235 | + |
| 236 | +## Gotchas & Anti-Patterns |
| 237 | + |
| 238 | +- Do **not** use Supabase Auth or add `middleware.ts` — use [`src/proxy.ts`](src/proxy.ts) |
| 239 | +- Do **not** fulfill purchases outside webhook handlers |
| 240 | +- Do **not** assume Redis or Upstash is always configured |
| 241 | +- Do **not** add RBAC without explicit request (flat admin today) |
| 242 | +- Do **not** trust root `README.md` — it is stale create-next-app boilerplate |
| 243 | +- CSP changes require updating allowlists in [`next.config.ts`](next.config.ts) (Stripe, Turnstile, Vercel Insights) |
| 244 | +- Guest checkout only — no customer accounts; buyers identified by email on `Purchase` |
| 245 | +- Legacy Pages Router health endpoint — do not migrate without request |
| 246 | + |
| 247 | +## Read More (When Working in These Areas) |
| 248 | + |
| 249 | +| Area | Source of truth | |
| 250 | +|------|-----------------| |
| 251 | +| Architecture & trade-offs | [`DESIGN.md`](DESIGN.md) | |
| 252 | +| Environment variables | [`.env.example`](.env.example) | |
| 253 | +| Database schema | [`prisma/schema.prisma`](prisma/schema.prisma) | |
| 254 | +| Money helpers | [`src/lib/money.ts`](src/lib/money.ts) | |
| 255 | +| Unit test mocks | [`src/lib/test/mocks/README.md`](src/lib/test/mocks/README.md) | |
| 256 | +| Payment providers | [`src/lib/payment/`](src/lib/payment/) | |
| 257 | +| Purchase fulfillment | [`src/lib/purchase-fulfillment.ts`](src/lib/purchase-fulfillment.ts) | |
0 commit comments