Skip to content

feat: production stack — billing, auth, email, observability, AI#23

Open
bryansayler wants to merge 30 commits into
mainfrom
claude/add-claude-documentation-biozg
Open

feat: production stack — billing, auth, email, observability, AI#23
bryansayler wants to merge 30 commits into
mainfrom
claude/add-claude-documentation-biozg

Conversation

@bryansayler
Copy link
Copy Markdown
Contributor

Summary

What began as a CLAUDE.md refresh grew into the scaffold's production spine. Each capability lands as its own stack of small, conventional commits and follows one repeated pattern: a lazy getX() client + assertXConfigured() guard, env vars .optional() so the scaffold still boots and bun run validates green with no .env, and a barrel index.ts.

Capabilities

  • Billing (Stripe)src/lib/billing/ (client/checkout/webhooks), webhook route, /pricing + checkout Server Action, customers/subscriptions tables.
  • Auth (Auth.js v5) — Drizzle adapter + Passkey (WebAuthn) + Resend magic-link, /signin UI, the four adapter tables + emailVerified/image on users.
  • Email (Resend + react-email)src/lib/email/sendEmail() (renders a React element or raw HTML), magic-link/welcome templates, email:dev preview.
  • Observability — zero-dep JSON logger, PostHog analytics (server + client, no-op when unconfigured), flags primitive, instrumentation.ts as the documented Sentry/OTel hook.
  • AI voice layersrc/lib/ai/: a cached Claude client (claude-opus-4-7) with a persona/register tone-composition layer and an animation-state vocabulary. Prompt-cache-ready (breakpoint on the frozen persona prefix). The one piece a competitor can't clone from package.json.
  • Docs — CLAUDE.md tracks every module, env var, and convention.

Security & CI fixes (this round)

  • HIGH — fix(deps): bumped drizzle-orm 0.38→0.45.2 / drizzle-kit→0.31.10, clearing GHSA-gpj5-g38j-94v9 (SQL injection via improperly escaped identifiers) — the advisory GitHub flagged on every push. Schema type-checks, all 7 tables still generate, suite passes under the new major.
  • CI — fix(ci): bun.lock was gitignored, but CI runs bun install --frozen-lockfile, which can't reproduce an install without it. Un-ignored and committed the lockfile, and aligned package.json caret floors to the installed-and-validated versions (floors were understating reality, e.g. next-auth beta.25 vs the beta.31 in use). Bonus: captures patched postcss 8.5.15.

Known follow-ups (deliberately not in this PR)

  • 3 moderate dev/build-time advisories remain (vite, esbuild, transitive postcss under next/vite). They need cross-major toolchain bumps (vite 6→7, vitest major) with no production-runtime exposure — better as a focused toolchain PR than slipped into a merge.
  • Deferred features documented in CLAUDE.md: Stripe Customer Portal, real Sentry SDK wiring, metered billing, an AI demo route.

Test plan

  • bun run validate green (lint + type-check + 9 tests + build)
  • bun run db:generate emits all 7 tables under drizzle-kit 0.31
  • bun audit — 0 high (was 1), 3 moderate dev-only remain
  • Stripe: stripe listen/pricing4242… → confirm subscriptions row
  • Auth: Passkey + magic-link round-trip with real AUTH_SECRET/RESEND_API_KEY
  • AI: exercise streamText against a real ANTHROPIC_API_KEY

https://claude.ai/code/session_01FHpnRnkmnWPyHLTm3ytQAN


Generated by Claude Code

claude added 29 commits April 22, 2026 17:36
Adds test:ui, test:e2e:ui, preview, and prepare scripts; documents the
src/__tests__/ and docs/recommendations/ directories; expands env,
testing, formatting, auth, and CI sections; notes that next.config.ts
imports src/lib/env.ts so validation runs at build/dev time.
Adds the stripe SDK and three optional server env vars
(STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_ID). All optional
so the scaffold continues to boot and validate without configuration;
wired projects can tighten to .min(1) when going live.
Introduces a customers table (1:1 with users, holds Stripe customer ID)
and a subscriptions table keyed on stripeSubscriptionId. Status is typed
via Stripe.Subscription.Status. userId is denormalized on subscriptions
for cheap lookups. Migration files are gitignored per existing policy
and regenerated per project via `bun run db:generate`.
New src/lib/billing/ module:
- client.ts: lazy Stripe instance + assertStripeConfigured() guard.
- checkout.ts: createCheckoutSession() that idempotently creates the
  Stripe customer + customers row, then opens a subscription Checkout.
- webhooks.ts: handleStripeEvent() with handlers for checkout completion,
  subscription created/updated/deleted, and invoice.payment_failed. Uses
  the post-dahlia API shape (current_period_end on items,
  invoice.parent.subscription_details.subscription).
- index.ts: barrel.
POST /api/webhooks/stripe verifies the signature against
STRIPE_WEBHOOK_SECRET, parses the event via getStripe().webhooks
.constructEvent, and dispatches to handleStripeEvent. Pinned to the
nodejs runtime and force-dynamic since Stripe needs Node and webhooks
must never be cached.
Server Component at /pricing renders a Subscribe button when
STRIPE_PRICE_ID is set, or an explicit configure-this notice when it
isn't, so a fresh scaffold makes the wiring step obvious. The Server
Action calls auth() and redirects unauthenticated users to signin,
otherwise creates a Stripe Checkout session and redirects to it.
Adds a Vitest test for the webhook route covering missing and invalid
signature paths. Env vars are set at the top of the test file because
src/lib/env.ts validates at module load. Documents the billing module,
local stripe-listen flow, and out-of-scope items in CLAUDE.md. Also
silences a pre-existing lint error on auto-generated next-env.d.ts that
surfaced with the Next 15.5 upgrade.
Pulls in @auth/drizzle-adapter, resend, and the @simplewebauthn
browser/server peers (pinned to ^9 to satisfy next-auth 5 beta).
Adds AUTH_EMAIL_FROM (magic-link sender) and RESEND_API_KEY env vars,
both optional so the scaffold continues to boot without configuration.
Adds emailVerified and image columns to users (required by the Auth.js
Drizzle adapter) plus four new tables: accounts, sessions,
verification_tokens, and authenticators (for WebAuthn passkeys). JS
property names match the adapter's expected shape so the tables can be
passed via defineTables().
New src/lib/email/ module:
- client.ts: lazy Resend instance + assertEmailConfigured() guard.
- index.ts: sendEmail() helper that throws on Resend API errors so
  callers get loud failures instead of silent no-ops.
Both AUTH_EMAIL_FROM and RESEND_API_KEY remain optional in env.ts so
the scaffold continues to validate without configuration; the guard
fires at first use.
Activates Auth.js with three pieces:
- DrizzleAdapter using the scaffold's existing users/accounts/sessions/
  verificationTokens/authenticators tables.
- Resend magic-link provider with a custom sendVerificationRequest that
  routes through src/lib/email/sendEmail() so all outgoing mail uses one
  code path.
- Passkey (WebAuthn) provider; requires session.strategy = "database"
  and experimental.enableWebAuthn = true.

Providers are wired but env vars stay optional — Auth.js will fail loudly
at signin if RESEND_API_KEY/AUTH_EMAIL_FROM aren't set, which is the
correct scaffold-time behaviour.
Server Component at /signin renders the PasskeyButton (client component
using next-auth/webauthn signIn) and a magic-link email form when
Resend env vars are set; otherwise shows an explicit configure-this
notice so a fresh scaffold makes wiring obvious.
Replaces the "scaffolded, no providers" auth section with the active
wiring (DrizzleAdapter + Resend magic-link + Passkey + /signin UI) and
adds an Email (Resend) section documenting the src/lib/email/ module
and local dev setup. Notes the @simplewebauthn peer-dep pin.
Adds @react-email/components and @react-email/render for building and
rendering typed email templates, react-email (dev) for the local
preview server, and an `email:dev` script that serves templates from
src/emails on port 3001.
Adds a shared EmailLayout (Tailwind-styled shell) plus MagicLinkEmail
and WelcomeEmail templates. Each template exports PreviewProps so the
`email:dev` preview server can render them with sample data.
Extends sendEmail to accept either `react` (a React element, rendered to
HTML + plaintext via @react-email/render) or raw `html`, as a
discriminated union so callers pass exactly one. Rewires the Auth.js
magic-link provider to use the MagicLinkEmail template, removing the
inline HTML string.
Zero-dependency leveled logger that emits single-line JSON so log drains
parse it directly. Level is controlled by the optional LOG_LEVEL env var
(defaults to info). The emit() body is the single swap point for
pino/Sentry without touching call sites. Also adds the optional PostHog
client env vars for the analytics module that follows.
Adds posthog-js/posthog-node and a unified analytics surface:
- src/lib/analytics.ts: server-side capture() and isFeatureEnabled(),
  both no-ops when NEXT_PUBLIC_POSTHOG_KEY is unset so call sites never
  guard.
- src/components/posthog-provider.tsx: client provider that inits
  posthog-js with pageview/pageleave capture, and renders children
  untouched when unconfigured.
- Wired PostHogProvider into the root Providers tree.
flag(name, { distinctId, default }) resolves env override
(FLAG_<UPPER_SNAKE>) → PostHog (when a distinctId is given) → default.
Reads process.env directly so projects add flags without editing the
typed env schema.
Adds src/instrumentation.ts exposing register() (boot-time agent init)
and onRequestError (server error capture routed through the logger) as
the documented Sentry/OpenTelemetry extension point — no Sentry SDK is
bundled, to keep the default install lean. Documents the email
templates and the full observability stack (logger, analytics, flags,
instrumentation) in CLAUDE.md, refreshes the directory tree and env
table, and adds the PostHog/LOG_LEVEL vars to .env.example.
Adds @anthropic-ai/sdk and the optional ANTHROPIC_API_KEY server env
var (optional so the scaffold keeps booting/validating without it).
Groundwork for the src/lib/ai persona/register layer.
client.ts exposes getAnthropic() (lazy, memoized, guarded by
assertAiConfigured()) and the DEFAULT_MODEL constant (claude-opus-4-7).
Lazy construction keeps the scaffold building without an API key.
personas.ts defines the stable, cacheable house voice. registers.ts
defines tonal modes (straight, deadpan, warm, hype, mock_panic, swoon)
layered on top per-moment — each carrying a short system directive plus
an animation vocabulary (waiting caption, motion character, intensity,
enter/exit timings) the UI animates against. Personality is meant to be
load-bearing: registers map to real state changes, not decoration.
compose.ts builds system as ordered blocks: the frozen persona prefix
carries the cache_control breakpoint, the register directive follows it
unmarked so it varies per-request without invalidating the cache.
generate.ts adds generateText() (non-streaming), createStream() (raw
MessageStream), and streamText() (async generator of text deltas).
Defaults are tuned for snappy tone responses: effort "low", thinking
off, claude-opus-4-7. No sampling params (removed on Opus 4.7).
animation.ts defines the AiState lifecycle (idle → summoning → streaming
→ settled / stumbled) and animationFor(state), which resolves the active
register's animation vocabulary so a UI can drive motion off real state
changes without knowing about registers directly. index.ts re-exports
the module surface.
Adds Vitest coverage for the pure logic: composeSystem places the
cache breakpoint on the persona prefix only, animationFor calms motion
once settled, and the register roster is exposed. Documents the
src/lib/ai voice layer, model/effort/caching notes, and the
ANTHROPIC_API_KEY env var in CLAUDE.md.
Ignores *.local.yaml / *.local.yml (consistent with the existing
.env*.local convention) so memory/profile.local.yaml — the agent's
real-usage signal for discovery Phase 2 — stays local by design.
Bumps drizzle-orm 0.38 -> 0.45.2 and drizzle-kit 0.30 -> 0.31.10 to
clear GHSA-gpj5-g38j-94v9 (high: SQL injection via improperly escaped
SQL identifiers) — the vulnerability GitHub flagged on every push.
Schema type-checks, all 7 tables still generate, and the suite passes
under the new major.
CI runs `bun install --frozen-lockfile`, which fails on a fresh clone
because bun.lock was gitignored — so the pipeline could not actually
reproduce an install. Un-ignores and commits the lockfile, and aligns
package.json caret floors to the installed-and-validated versions (the
floors were understating what was already resolved, e.g. next-auth
beta.25 vs the beta.31 in use). This also captures the patched
drizzle-orm 0.45.2 and postcss 8.5.15. Full `bun run validate` is green.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0e31d26f33

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/app/pricing/page.tsx
Comment thread src/lib/email/client.ts Outdated
@bryansayler
Copy link
Copy Markdown
Contributor Author

@codex address the feedback and optimize the process, architecture, and load bearing foundational components

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

The lazy getX() + assertXConfigured() provider pattern was hand-rolled
three times (billing, email, ai) — the top "invest" signal from real
usage. createLazyClient({ name, requires, create }) is now the single
home for it: memoized lazy construction, deferred until first use, and
an aggregated "<name> not configured: set X, Y" guard. Billing, Email,
and AI clients become thin wrappers; future integrations follow suit.
Adds factory tests (memoization, aggregated error, guard-before-construct).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants