# CLAUDE.md — `web/web/`

The Astro 6 + React 19 site deployed to https://one.ie. This is `one-prod` in package.json. Parent `web/CLAUDE.md` covers the agents/ ↔ web/ split — read it first.

Stack one-liner: Astro 6 SSR on CF Workers (`@astrojs/cloudflare`, `output: 'server'`) + React 19 islands + Tailwind 4 + shadcn/ui + AI SDK v6.

## Layout

| Path | What |
|------|------|
| `src/pages/*.astro` | ~40 top-level marketing + product pages (see route map below) |
| `src/pages/api/` | 50+ route folders + ~40 flat handlers — single source of truth for backend |
| `src/layouts/Layout.astro` | The only layout — header/footer/theme |
| `src/components/` | React islands grouped by surface: `chat/`, `crm/`, `pay/`, `dashboard/`, `funnel/`, `ui/` (shadcn), etc. |
| `src/actions/` | Astro server actions — `auth.ts`, `billing.ts`, `agents.ts`, `people.ts`, `skills.ts`, `tools.ts`, `settings.ts`, `diagnostics.ts`; wired via `index.ts` |
| `src/workers/` | Cron + DO modules bundled into the same Worker via `durableObjectExportsPlugin` (see `astro.config.mjs`) — billing crons, funnel aggregate, webhook deliver, `AnalyticsRelay` DO |
| `src/middleware.ts` | Workspace resolution + session + visitor hashing — runs every SSR request |
| `src/lib/` | Shared server utils (`passkey`, `billing-config`, `trace`, `types`) + authority helpers (`authority.ts`, `workspace-authority.ts`, `viewer.ts`, `role-check.ts`) — thin callers over `schema/roles.tql` |
| `migrations/` | D1 schema (`one-owners` DB) |

## Route map (top-level pages)

| Family | Pages |
|--------|-------|
| Marketing | `index`, `agency`, `agents`, `chatbots`, `marketplace`, `partners`, `payments`, `showcase`, `developers`, `brand`, `design`, `models`, `tools`, `skills`, `teams`, `vietnam` |
| Product | `chat`, `create`, `dashboard`, `settings`, `analytics`, `tracking`, `memory`, `learning`, `motion`, `scale`, `security`, `auth-security`, `recovery-codes`, `get-yours` |
| Dynamic | `u/`, `o/`, `in/`, `go/`, `peer/`, `studio/`, `share/`, `crm/`, `features/`, `settings/` (folder routes) |
| System | `404`, `500`, `cc` |

## Middleware contract (`src/middleware.ts`)

Resolves four host shapes, sets `ctx.locals.{slug, workspaceContext, visitorHash, session}`:

| Host | Behavior |
|------|---------|
| `one.ie` / localhost | Primary — no workspace; viewer via `deriveViewer` |
| `app.one.ie` | Authenticated workspace; anonymous → `demo` workspace |
| `{slug}.one.ie` | Subdomain workspace; loads billing + descendants from KV + D1 |
| Custom domain | D1 `domains` lookup → rewrites to `/u/{slug}{path}` |

**Viewer vantage ladder:** `platform → agency → client → end_user`, set by `deriveViewer` in `src/lib/viewer.ts`. `agency` is derived from the D1 `parent_slug` ancestor-chain (`controlsAncestor`) at any depth — not merely from login state. This is the same walk-up authority model in `text/roles-authority-plan.md` / `schema/roles.tql`. Don't confuse the viewer *vantage* with the membership *role* `owner`.

Session is HMAC-signed cookie via `lib/passkey.readSession`, with KV fallback. `DEV_SLUG` in `dev.vars` bypasses auth on localhost. Visitor cookie `vid` is always hashed before logging.

## API routes (`src/pages/api/`)

All backend lives here — no separate API worker. ~50 route folders mirror the 6 verbs and dimensions: `signal/`, `mark/`, `warn/`, `follow/`, `forget/`, `fade.ts`; plus surface families: `chat/`, `pay/`, `payments/`, `billing/`, `agents/`, `actors/`, `things/`, `paths/`, `crm/`, `identity/`, `provision/`, `webhook/`, `x402.ts`. See `.claude/rules/api.md` for handler shape.

## Server actions

Used by React forms via `astro:actions`. Each module in `src/actions/` exports a `defineAction` map; `index.ts` is the registry. Prefer actions over fetch+API for same-origin mutations from islands.

## Client islands

React components are imported into `.astro` files with a `client:*` directive (`client:load`, `client:idle`, `client:visible`, `client:only="react"`). The `manualChunks` config in `astro.config.mjs` splits `react-vendor`, `ai-vendor`, `icons`, and a `sidebar` chunk; respect these boundaries — don't import heavy deps into the sidebar chunk.

Heavy SSR-unfriendly libs (`recharts`, `@stripe/*`, `leaflet`, `shiki`, `media-chrome`, `motion`) are SSR-externalized via `ssrExternalPlugin` — they must be wrapped in `client:only="react"` islands.

**Auth-gated workspace pages (`/u/[slug]/*`, `/settings/*`, `/crm/c/*`)** should use `client:only="react"` rather than `client:load` — there is no SEO requirement for authed pages, and `client:only` keeps the React component tree out of the SSR worker bundle entirely.

## Commands

| Command | What |
|---------|------|
| `bun run dev` | Astro dev on default port |
| `bun run dev:full` | Dev + local gateway (`scripts/local-gateway.ts`) in background |
| `bun run dev:wrangler` | Build + `wrangler dev` on 8787 (use to test DOs + crons) |
| `bun run build` | `astro build` (production) |
| `bun run deploy` | Build + `wrangler deploy` → https://one.ie |
| `bun run verify` | `tsc --noEmit` + `vitest run` — required gate before close |
| `bun run db:migrate:local` | Apply D1 migrations to local `one-owners` (migrations run 0001–0082; note the script's range is stale at 0035–0045 — widen before relying on it) |
| `bun run test:e2e:pw` | Playwright E2E |

## Don't

- Don't create new top-level `.astro` pages without a route plan — already ~40
- Don't add a second layout; extend `Layout.astro`
- Don't add backend routes outside `src/pages/api/` or `src/actions/`
- Don't import SSR-externalized libs in `.astro` frontmatter — islands only
- Don't set `main` in `wrangler.toml` — Astro's vite plugin owns the entry
- Don't set `build: { inlineStylesheets: 'always' }` — under the CF adapter it duplicates the full Tailwind sheet into every route's manifest entry and blows the worker bundle by 8+ MiB. Keep it `'auto'` (or omit — `'auto'` is Astro's default). See `.claude/commands/deploy.md § Bundle Size Rule 4`.
- Don't ship code that references a D1 column after a DROP+CREATE migration without grepping for the old name first. TypeScript can't catch schema drift; D1 errors only surface at runtime as `no such column`. After any DROP+CREATE migration (e.g. `0059_domains.sql` renamed `slug`→`gid`), grep for the old column across `src/` and update call sites BEFORE deploying.

## Credentials

All secrets (GATEWAY_API_KEY, TYPEDB_URL/USERNAME/PASSWORD, IDENTITY_SECRET, D1 binding names, etc.) live in `one-ie/one.ie/.env`. Scripts that need prod access should load that file. Never hardcode values here.

## See also

- `web/CLAUDE.md` — agents/ ↔ web/ relationship
- `.claude/rules/api.md` — API handler shape
- `.claude/rules/ui.md` — `emitClick` convention for islands
- `astro.config.mjs` — DO bundling + chunk strategy
