feat: production stack — billing, auth, email, observability, AI#23
Open
bryansayler wants to merge 30 commits into
Open
feat: production stack — billing, auth, email, observability, AI#23bryansayler wants to merge 30 commits into
bryansayler wants to merge 30 commits into
Conversation
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.
There was a problem hiding this comment.
💡 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".
Contributor
Author
|
@codex address the feedback and optimize the process, architecture, and load bearing foundational components |
|
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 andbun run validates green with no.env, and a barrelindex.ts.Capabilities
src/lib/billing/(client/checkout/webhooks), webhook route,/pricing+ checkout Server Action,customers/subscriptionstables./signinUI, the four adapter tables +emailVerified/imageonusers.src/lib/email/sendEmail()(renders a React element or raw HTML),magic-link/welcometemplates,email:devpreview.logger, PostHoganalytics(server + client, no-op when unconfigured),flagsprimitive,instrumentation.tsas the documented Sentry/OTel hook.src/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 frompackage.json.Security & CI fixes (this round)
fix(deps): bumpeddrizzle-orm0.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.fix(ci):bun.lockwas gitignored, but CI runsbun install --frozen-lockfile, which can't reproduce an install without it. Un-ignored and committed the lockfile, and alignedpackage.jsoncaret 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 patchedpostcss8.5.15.Known follow-ups (deliberately not in this PR)
vite,esbuild, transitivepostcssundernext/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.Test plan
bun run validategreen (lint + type-check + 9 tests + build)bun run db:generateemits all 7 tables under drizzle-kit 0.31bun audit— 0 high (was 1), 3 moderate dev-only remainstripe listen→/pricing→4242…→ confirmsubscriptionsrowAUTH_SECRET/RESEND_API_KEYstreamTextagainst a realANTHROPIC_API_KEYhttps://claude.ai/code/session_01FHpnRnkmnWPyHLTm3ytQAN
Generated by Claude Code