Single-tenant marketing site for WebLingo, a SaaS localization product. Built with Next.js (App Router), Tailwind CSS, and Stripe Checkout. The codebase follows a “proto-package” layout so internal modules can be promoted to standalone packages when new SaaS sites spin up. The Supabase-authenticated customer dashboard lives alongside the marketing site and communicates with the Cloudflare worker (webhooks API) for site management.
app/ # Next.js routes (localized under /[locale]) and API handlers
components/ # Reusable UI components for the site
internal/ # Proto-packages (env, billing, etc.)
core/
billing/
i18n/
modules/ # Feature modules (e.g., pricing)
styles/ # Global styles (Tailwind)
Key modules today:
internal/core/env.ts— strict environment variable parsinginternal/billing/stripe.ts— Stripe client + helpersinternal/i18n/— Locale config, message loaders, translation helpersmodules/pricing/— Pricing tier definitions and UIcomponents/ui/— Locally vendored shadcn/ui primitives (button, card, input, badge)components/pricing-teaser.tsx— Home page pricing teaser built on shadcn/ui cards
Internationalization is handled via the /[locale] segment with English, French, and Japanese dictionaries stored under internal/i18n/messages/*.
- Env source of truth:
internal/core/env.ts(client) andinternal/core/env-server.ts(server). - Dashboard capability matrix/spec:
docs/backend/DASHBOARD_SPECS.md. - Backend docs snapshot sync:
WEBLINGO_REPO_PATH=/absolute/path/to/weblingo corepack pnpm docs:syncWEBLINGO_REPO_PATH=/absolute/path/to/weblingo corepack pnpm docs:sync:check
- Minimal validation for docs/capability alignment:
corepack pnpm test:contractscorepack pnpm checkWEBLINGO_REPO_PATH=/absolute/path/to/weblingo corepack pnpm check:ci
Type-aware ESLint now uses projectService: true for TypeScript files.
- First-wave strict rules apply to
app/api/**/*.ts,internal/**/*.ts, andlib/**/*.ts. - First-wave rules:
consistent-type-imports,no-floating-promises,no-misused-promises,no-unnecessary-condition, and phasedstrict-boolean-expressions. - Broad TSX
strict-boolean-expressionsremains deferred forcomponents/**,modules/**, route TSX files, andinternal/**/*.tsxuntil ReactNode truthiness patterns are normalized. corepack pnpm check:ciaddstest:contractsand runsdocs:sync:checkwhenWEBLINGO_REPO_PATHis set.
Create .env.local (or configure host env) with:
# App + dashboard
NEXT_PUBLIC_APP_URL=http://localhost:3000
HOME_PAGE_VARIANT=expansion # optional: classic | expansion (default expansion)
PUBLIC_PORTAL_MODE=enabled # required: enabled | disabled
# Public endpoint consumed by the dashboard UI. Protect with auth, CORS, and rate limits.
NEXT_PUBLIC_WEBHOOKS_API_BASE=https://api.weblingo.app/api
NEXT_PUBLIC_WEBHOOKS_API_TIMEOUT_MS=15000
TRY_NOW_TOKEN=preview_token_value
# Public form abuse controls
WEBSITE_WAITLIST_RATE_LIMIT_WINDOW_MS=60000
WEBSITE_WAITLIST_MAX_PER_WINDOW=20
WEBSITE_WAITLIST_MAX_BODY_BYTES=4096
WEBSITE_CONTACT_RATE_LIMIT_WINDOW_MS=60000
WEBSITE_CONTACT_MAX_PER_WINDOW=10
# Preview abuse controls (required when TRY_NOW_TOKEN is set)
WEBSITE_PREVIEW_RATE_LIMIT_WINDOW_MS=60000
WEBSITE_PREVIEW_CREATE_MAX_PER_WINDOW=20
WEBSITE_PREVIEW_CREATE_MAX_PER_SOURCE_HOST_PER_WINDOW=10
WEBSITE_PREVIEW_STATUS_MAX_PER_WINDOW=120
WEBSITE_PREVIEW_STREAM_MAX_PER_WINDOW=30
WEBSITE_PREVIEW_MAX_BODY_BYTES=16384
WEBSITE_PREVIEW_UPSTREAM_CREATE_TIMEOUT_MS=15000
WEBSITE_PREVIEW_UPSTREAM_STATUS_TIMEOUT_MS=15000
WEBSITE_PREVIEW_UPSTREAM_STREAM_CONNECT_TIMEOUT_MS=15000
# Redis (required; preferred Vercel/Upstash naming)
UPSTASH_REDIS__KV_REST_API_URL=https://<db>.upstash.io
UPSTASH_REDIS__KV_REST_API_TOKEN=upstash_token
# Stripe
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICING_TABLE_ID=prctbl_default
STRIPE_PRICING_TABLE_ID_EN=prctbl_for_en
STRIPE_PRICING_TABLE_ID_FR=prctbl_for_fr
STRIPE_PRICING_TABLE_ID_JA=prctbl_for_ja
# Supabase (auth + logging)
NEXT_PUBLIC_SUPABASE_URL=https://<project>.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=public-anon-key
SUPABASE_SECRET_KEY=service-role-key
SUPABASE_AUTH_TIMEOUT_MS=15000
# Analytics (optional)
NEXT_PUBLIC_POSTHOG_KEY=phc_...
# Required at build time. Use https://metrics.weblingo.app in production/preview.
NEXT_PUBLIC_POSTHOG_BROWSER_HOST=http://localhost:3000/_analytics/posthog
# Kill switch for browser and server analytics capture.
NEXT_PUBLIC_POSTHOG_CAPTURE=enabled
# Upstream ingestion host for server analytics and the local fallback proxy route.
NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
# Session replay is disabled unless explicitly sampled on allowlisted public routes.
NEXT_PUBLIC_POSTHOG_REPLAY_CAPTURE=disabled
NEXT_PUBLIC_POSTHOG_REPLAY_SAMPLE_RATE=0
PUBLIC_PORTAL_MODE=disabled hides the login CTA, disables signup/login actions, blocks checkout, and returns 404 for the login and dashboard onboarding screens. Set it to enabled to expose the portal.
PostHog browser traffic uses NEXT_PUBLIC_POSTHOG_BROWSER_HOST as the SDK api_host. Because NEXT_PUBLIC_* values are baked into the Next.js client bundle at build time, set this variable in every deployed build target before deploying. Production and preview should point it at the managed proxy (https://metrics.weblingo.app); local development can keep using the first-party http://localhost:3000/_analytics/posthog route. Keep NEXT_PUBLIC_POSTHOG_HOST pointed at the upstream PostHog ingestion host (for example https://eu.i.posthog.com), not the browser-facing proxy path. NEXT_PUBLIC_POSTHOG_CAPTURE=disabled is the analytics kill switch. Session replay remains off unless NEXT_PUBLIC_POSTHOG_REPLAY_CAPTURE=sampled and NEXT_PUBLIC_POSTHOG_REPLAY_SAMPLE_RATE is a value from 0 to 1; replay is still limited by the route allowlist in internal/analytics/replay.ts.
See docs/POSTHOG_ANALYTICS.md for the event/property contract, replay allowlist, identity grouping, and PostHog MCP guardrails.
- Install dependencies:
corepack pnpm install - Fill
.env.localwith the values above (setNEXT_PUBLIC_WEBHOOKS_API_BASEfor the dashboard andTRY_NOW_TOKENfor previews). Redis credentials are required for public-form and preview rate limiting (UPSTASH_REDIS__KV_REST_API_URL+UPSTASH_REDIS__KV_REST_API_TOKEN). - Start dev server:
corepack pnpm run dev(openshttp://localhost:3000). - Dashboard access: visit
/dashboard, sign in via Supabase auth, then create/manage sites (callsNEXT_PUBLIC_WEBHOOKS_API_BASE). - Validation: optional
corepack pnpm run lint,corepack pnpm run typecheck,corepack pnpm run formatbefore committing.
- Run
corepack pnpm test:e2e:smoke. - This command sets
DASHBOARD_E2E_MOCK=1, which bypasses Supabase login and serves deterministic mocked webhook payloads for dashboard smoke paths/actions. - Use this for CI/local smoke coverage of dashboard routing + action feedback without depending on live auth/API infrastructure.
Website API docs use backend-synced snapshots under content/docs/_generated.
- Refresh snapshots:
WEBLINGO_REPO_PATH=/absolute/path/to/weblingo corepack pnpm docs:sync
- Verify snapshots are fresh (fails fast when missing/stale):
WEBLINGO_REPO_PATH=/absolute/path/to/weblingo corepack pnpm docs:sync:check
- Contract/doc coverage tests:
corepack pnpm test:contracts
WEBLINGO_REPO_PATH is required for sync commands. There is no fallback path.
CI behavior:
- When
CROSS_REPO_CHECKOUT_TOKENis available,.github/workflows/ci.ymlchecks out backend HEAD and runs:WEBLINGO_REPO_PATH="$GITHUB_WORKSPACE/backend" corepack pnpm docs:sync:checkcorepack pnpm test:contracts
- When token access is unavailable, run this fallback locally before opening/updating a PR:
WEBLINGO_REPO_PATH=/absolute/path/to/weblingo corepack pnpm check:ci
Ownership:
- Website maintainers own
content/docs/_generated/*, docs pages, and dashboard capability matrix updates. - Backend maintainers own canonical OpenAPI/feature catalog/playbooks and must notify website maintainers on user-facing contract changes.
- Current maintainer model: one repository maintainer currently fulfills both roles.
- Canonical website capability matrix/spec:
docs/backend/DASHBOARD_SPECS.md. - Backend bridge pointer to this matrix:
weblingo/docs/backend/DASHBOARD_SPECS.md.
- Create recurring prices for the public customer plans and keep the self-serve contract aligned with the backend: one account/subscription covers one website. Additional websites require separate scoping or agency handling.
- Populate the matching IDs in
modules/pricing/data.ts. - Run
stripe listen --forward-to localhost:3000/api/stripe/webhookfor local development and set theSTRIPE_WEBHOOK_SECRET. - Configure Stripe Pricing Tables for each locale (or reuse one) and copy the IDs into
STRIPE_PRICING_TABLE_ID(fallback) and the locale-specific envs (STRIPE_PRICING_TABLE_ID_EN,FR,JA). The embed renders on/[locale]/pricing, with the in-app comparison table acting as a fallback.
- Build:
corepack pnpm run build(Next.js static + server output). - Hosting: Deploy to Vercel/Netlify/Fly/etc. with Node 20.9+ and set all env vars above. Ensure the hosting URL matches
NEXT_PUBLIC_APP_URL. - Rate limiting store: Provision Upstash Redis (or compatible REST KV) and configure
UPSTASH_REDIS__KV_REST_API_URL/UPSTASH_REDIS__KV_REST_API_TOKEN. This is required for waitlist/contact/preview abuse controls. - Supabase: Configure the site URL and redirect URLs in Supabase Auth settings. Provide
NEXT_PUBLIC_SUPABASE_URL/NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY/SUPABASE_SECRET_KEYto the host. - Worker API: Point
NEXT_PUBLIC_WEBHOOKS_API_BASEto the livewebhooksworker; enable CORS for the dashboard origin. UseTRY_NOW_TOKENfor server-side preview requests. - Stripe: Add webhook endpoint pointing to
/api/stripe/webhookand setSTRIPE_WEBHOOK_SECRETon the host. - Dashboard:
/dashboarduses Supabase session cookies; ensure the domain matches your Supabase config so auth cookies persist. - Smoke checks: After deploy, verify
/[locale]pages load, Stripe Pricing Table renders, and the dashboard shows no-site onboarding or routes a one-site customer directly to the website workspace.
- Extract modules from
internal/*into/packages/*when a second SaaS app appears. - Add authentication module (e.g., Supabase) under
internal/authonce customer portals launch. - Extend billing module with customer portal API when needed.