-
Notifications
You must be signed in to change notification settings - Fork 0
feat: production stack — billing, auth, email, observability, AI #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bryansayler
wants to merge
30
commits into
main
Choose a base branch
from
claude/add-claude-documentation-biozg
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
263949b
docs: expand CLAUDE.md with additional tooling details
claude d3a564a
feat(billing): add stripe dependency and env vars
claude d778774
feat(db): add customers and subscriptions tables
claude 477f61d
feat(billing): add stripe client, checkout helper, and webhook handlers
claude ba9133c
feat(billing): add stripe webhook route handler
claude f35b156
feat(billing): add /pricing route and checkout server action
claude 6d126ed
feat(billing): test webhook signature verification and document setup
claude bcd280c
feat(auth): add drizzle adapter, resend, and simplewebauthn deps
claude cd092c4
feat(db): add auth.js drizzle adapter tables and extend users
claude 15ea897
feat(email): add resend client and sendEmail helper
claude 26af1da
feat(auth): wire drizzle adapter, resend magic link, and passkey
claude 021eab9
feat(auth): add /signin page with passkey and magic-link flows
claude 9d61685
docs(auth): document the auth + email stack in CLAUDE.md
claude b462dbc
feat(email): add react-email libraries and preview script
claude dba12cf
feat(email): add react-email templates for magic link and welcome
claude a6a0151
feat(email): render react templates via sendEmail and use in auth
claude 1828bfd
feat(observability): add structured logger
claude e88b957
feat(observability): add posthog analytics (server + client)
claude d50fd9e
feat(observability): add feature-flag primitive
claude f659121
feat(observability): add instrumentation hook and document the stack
claude 6723488
feat(ai): add anthropic sdk dependency and env var
claude d154038
feat(ai): add lazy anthropic client
claude bf12f90
feat(ai): add persona and register tone-preset layer
claude e958516
feat(ai): add cache-ready system composer and generate helpers
claude d7e4c81
feat(ai): add animation-state vocabulary and barrel
claude 7edadb9
feat(ai): test compose/animation logic and document the module
claude a359ca2
chore: gitignore local-only signal/profile files
claude f84342a
fix(deps): patch drizzle-orm SQL-injection advisory
claude 0e31d26
fix(ci): commit bun.lock and align dependency floors
claude 59bb459
refactor(lib): extract shared lazy-client factory
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| // @vitest-environment node | ||
| // Env vars MUST be set before any import that loads src/lib/env.ts, | ||
| // because env validation runs at module load. | ||
| process.env.STRIPE_SECRET_KEY = "sk_test_dummy"; | ||
| process.env.STRIPE_WEBHOOK_SECRET = "whsec_test"; | ||
|
|
||
| import { describe, it, expect } from "vitest"; | ||
| import { POST } from "./route"; | ||
|
|
||
| describe("POST /api/webhooks/stripe", () => { | ||
| it("returns 400 when the stripe-signature header is missing", async () => { | ||
| const req = new Request("http://localhost/api/webhooks/stripe", { | ||
| method: "POST", | ||
| body: "{}", | ||
| }); | ||
| const res = await POST(req); | ||
| expect(res.status).toBe(400); | ||
| }); | ||
|
|
||
| it("returns 400 when the signature is invalid", async () => { | ||
| const req = new Request("http://localhost/api/webhooks/stripe", { | ||
| method: "POST", | ||
| headers: { "stripe-signature": "t=1,v1=deadbeef" }, | ||
| body: '{"id":"evt_test"}', | ||
| }); | ||
| const res = await POST(req); | ||
| expect(res.status).toBe(400); | ||
| }); | ||
| }); |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { NextResponse } from "next/server"; | ||
| import { getStripe, handleStripeEvent } from "@/lib/billing"; | ||
| import { env } from "@/lib/env"; | ||
|
|
||
| export const runtime = "nodejs"; | ||
| export const dynamic = "force-dynamic"; | ||
|
|
||
| export async function POST(req: Request): Promise<Response> { | ||
| const sig = req.headers.get("stripe-signature"); | ||
| const secret = env.STRIPE_WEBHOOK_SECRET; | ||
| if (!sig || !secret) { | ||
| return new NextResponse("Missing signature or secret", { status: 400 }); | ||
| } | ||
|
|
||
| const body = await req.text(); | ||
| let event; | ||
| try { | ||
| event = getStripe().webhooks.constructEvent(body, sig, secret); | ||
| } catch (err) { | ||
| const msg = err instanceof Error ? err.message : "Invalid signature"; | ||
| return new NextResponse(`Webhook Error: ${msg}`, { status: 400 }); | ||
| } | ||
|
|
||
| try { | ||
| await handleStripeEvent(event); | ||
| } catch (err) { | ||
| console.error("[stripe] handler error", err); | ||
| return new NextResponse("Handler error", { status: 500 }); | ||
| } | ||
| return NextResponse.json({ received: true }); | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| "use server"; | ||
|
|
||
| import { redirect } from "next/navigation"; | ||
| import { auth } from "@/lib/auth"; | ||
| import { env } from "@/lib/env"; | ||
| import { createCheckoutSession } from "@/lib/billing"; | ||
|
|
||
| export async function startCheckout(): Promise<void> { | ||
| const session = await auth(); | ||
| if (!session?.user?.id || !session.user.email) { | ||
| redirect("/api/auth/signin"); | ||
| } | ||
| if (!env.STRIPE_PRICE_ID) { | ||
| throw new Error("STRIPE_PRICE_ID is not configured"); | ||
| } | ||
|
|
||
| const appUrl = env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"; | ||
| const checkout = await createCheckoutSession({ | ||
| userId: session.user.id, | ||
| email: session.user.email, | ||
| priceId: env.STRIPE_PRICE_ID, | ||
| successUrl: `${appUrl}/pricing?status=success`, | ||
| cancelUrl: `${appUrl}/pricing?status=cancel`, | ||
| }); | ||
|
|
||
| redirect(checkout.url); | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { env } from "@/lib/env"; | ||
| import { startCheckout } from "./actions"; | ||
|
|
||
| export default function PricingPage() { | ||
| const configured = Boolean(env.STRIPE_PRICE_ID); | ||
|
|
||
| return ( | ||
| <main className="flex min-h-screen flex-col items-center justify-center gap-6 p-24"> | ||
| <h1 className="text-3xl font-bold tracking-tight">Pricing</h1> | ||
| <div className="border-border bg-card w-full max-w-md rounded-lg border p-6 shadow-sm"> | ||
| <h2 className="text-xl font-semibold">Pro</h2> | ||
| <p className="text-muted-foreground mt-1 text-sm"> | ||
| $X / month — replace with your product copy. | ||
| </p> | ||
| {configured ? ( | ||
| <form action={startCheckout} className="mt-6"> | ||
| <button | ||
| type="submit" | ||
| className="bg-primary text-primary-foreground hover:bg-primary/90 w-full rounded-md px-4 py-2 text-sm font-medium" | ||
| > | ||
| Subscribe | ||
| </button> | ||
| </form> | ||
| ) : ( | ||
| <p className="text-muted-foreground mt-6 text-xs"> | ||
| Configure <code>STRIPE_PRICE_ID</code> in <code>.env</code> to | ||
| enable checkout. | ||
| </p> | ||
| )} | ||
| </div> | ||
| </main> | ||
| ); | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| "use server"; | ||
|
|
||
| import { signIn } from "@/lib/auth"; | ||
|
|
||
| export async function sendMagicLink(formData: FormData): Promise<void> { | ||
| const email = formData.get("email"); | ||
| if (typeof email !== "string" || !email) { | ||
| throw new Error("Email is required"); | ||
| } | ||
| await signIn("resend", { email, redirectTo: "/" }); | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import { env } from "@/lib/env"; | ||
| import { sendMagicLink } from "./actions"; | ||
| import { PasskeyButton } from "./passkey-button"; | ||
|
|
||
| export default function SignInPage() { | ||
| const emailConfigured = Boolean(env.RESEND_API_KEY && env.AUTH_EMAIL_FROM); | ||
|
|
||
| return ( | ||
| <main className="flex min-h-screen flex-col items-center justify-center p-24"> | ||
| <div className="border-border bg-card w-full max-w-sm rounded-lg border p-6 shadow-sm"> | ||
| <h1 className="text-2xl font-bold tracking-tight">Sign in</h1> | ||
| <p className="text-muted-foreground mt-1 text-sm"> | ||
| Use a passkey or a one-time link. | ||
| </p> | ||
|
|
||
| <div className="mt-6 space-y-3"> | ||
| <PasskeyButton /> | ||
|
|
||
| <div className="text-muted-foreground flex items-center gap-3 text-xs"> | ||
| <div className="bg-border h-px flex-1" /> | ||
| or | ||
| <div className="bg-border h-px flex-1" /> | ||
| </div> | ||
|
|
||
| {emailConfigured ? ( | ||
| <form action={sendMagicLink} className="space-y-3"> | ||
| <input | ||
| type="email" | ||
| name="email" | ||
| required | ||
| placeholder="you@example.com" | ||
| className="border-input bg-background w-full rounded-md border px-3 py-2 text-sm" | ||
| /> | ||
| <button | ||
| type="submit" | ||
| className="bg-primary text-primary-foreground hover:bg-primary/90 w-full rounded-md px-4 py-2 text-sm font-medium" | ||
| > | ||
| Email me a sign-in link | ||
| </button> | ||
| </form> | ||
| ) : ( | ||
| <p className="text-muted-foreground text-xs"> | ||
| Configure <code>RESEND_API_KEY</code> and{" "} | ||
| <code>AUTH_EMAIL_FROM</code> in <code>.env</code> to enable email | ||
| sign-in. | ||
| </p> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </main> | ||
| ); | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| "use client"; | ||
|
|
||
| import { signIn } from "next-auth/webauthn"; | ||
| import { useState } from "react"; | ||
|
|
||
| export function PasskeyButton() { | ||
| const [pending, setPending] = useState(false); | ||
|
|
||
| return ( | ||
| <button | ||
| type="button" | ||
| disabled={pending} | ||
| onClick={async () => { | ||
| setPending(true); | ||
| try { | ||
| await signIn("passkey", { redirectTo: "/" }); | ||
| } finally { | ||
| setPending(false); | ||
| } | ||
| }} | ||
| className="border-input hover:bg-accent w-full rounded-md border px-4 py-2 text-sm font-medium disabled:opacity-50" | ||
| > | ||
| {pending ? "Authenticating…" : "Sign in with a passkey"} | ||
| </button> | ||
| ); | ||
| } |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.