Web App
Client-only Next.js SPA with route groups for public marketing and authenticated app shell, strict constraints against server-side code, typed API clients, and centralized browser utilities.
Web App (apps/web)
The web application is a client-only Next.js SPA. It acts as a thin presentation layer that consumes the API server directly. No server-side rendering, no API proxying, no middleware.
Route Groups
The app uses Next.js route groups to separate public marketing pages from the authenticated application:
(website) — Public Marketing
Pages under app/(website)/ are publicly accessible and wrapped in the WebsiteHeader + WebsiteFooter layout:
| Page | Path | Description |
|---|---|---|
| Landing | / | Config-driven hero, features grid, FAQ, CTA section with JSON-LD structured data |
| Blog listing | /blog | Blog articles with category filtering, skeleton loading, empty state |
| Blog post | /blog/[slug] | Individual article with author info, tags, hero image |
| Pricing | /pricing | 3-tier pricing (Free/Pro/Enterprise) with FAQ section |
| Terms | /terms | Terms of Service (config-driven company references) |
| Privacy | /privacy | Privacy Policy (config-driven contact emails) |
(application) — Authenticated App
Pages under app/(application)/ are behind an auth-guard layout that waits for session bootstrap, restores via the API's HttpOnly refresh cookie when possible, and redirects to /sign-in if the browser is still unauthenticated:
| Page | Path | Description |
|---|---|---|
| Org redirect | /org | Smart redirect: resolves first org → first team → dashboard |
| Workspaces | /org/[orgId]/workspaces | Team listing and creation within an organization |
| Team dashboard | /org/[orgId]/[teamId] | Dashboard for a specific team |
Auth Pages
Auth pages (sign-in, sign-up, forgot-password, reset-password) live at the app root, outside both route groups. They use a split-panel layout with the form on the left and a branded panel on the right.
Config-Driven Content
Site-wide configuration lives in apps/web/config/index.ts:
export const config: SiteConfig = {
name: 'tx-agent-kit',
domain: 'tx-agent-kit.dev',
description: '...',
company: { name, supportEmail, legalEmail, privacyEmail, address, phone },
homepage: { heroTitle, heroSubtitle, features, faqs, ctaTitle, ctaDescription },
blog: { title, description },
dashboard: { sidebarNavItems }
}No Stripe price IDs are stored in the config. The header, footer, landing page, and legal pages all reference this config.
SEO Infrastructure
| File | Purpose |
|---|---|
lib/seo.ts | buildTitle, buildDescription, Organization/WebPage/BreadcrumbList/FAQPage structured data builders |
lib/blog.ts | Backend-agnostic BlogDataSource interface, estimateReadingTime, escapeXml |
lib/blog-seo.ts | BlogPosting and Blog JSON-LD structured data builders |
components/Breadcrumbs.tsx | Breadcrumb navigation with chevron separators and aria-label |
components/StructuredData.tsx | JSON-LD <script type="application/ld+json"> renderer |
app/sitemap.ts | Sitemap with all marketing page URLs |
app/robots.ts | Robots.txt disallowing authenticated routes |
Blog Data Layer
The blog system is backend-agnostic. Consumers provide a BlogDataSource implementation:
export interface BlogDataSource {
getArticles: (limit?: number, categoryId?: string) => Promise<BlogArticle[]>
getArticleBySlug: (slug: string) => Promise<BlogArticle | null>
getCategories: () => Promise<BlogCategory[]>
getCategoryBySlug: (slug: string) => Promise<BlogCategory | null>
}Connect any backend (PocketBase, Supabase, filesystem MDX, headless CMS) by calling setBlogDataSource(source) at app initialization.
Hard Constraints
These constraints are mechanically enforced by ESLint and structural checks:
| Constraint | Detail |
|---|---|
| No server-side code | app/api routes, proxy.ts, middleware.ts, next/server, and next/headers are all forbidden |
| Client-only components | Every .tsx file under app/ and components/ must start with 'use client' |
| No Effect or Drizzle | The web app must not import effect, effect/*, or drizzle-orm. Keep runtime complexity in the API/core/worker layers |
| No direct fetch | All API communication goes through typed client layers generated by Orval |
Centralized Utilities
To prevent scattered usage of browser APIs and third-party libraries, the web app enforces single entry points for common concerns:
Storage (lib/auth-token.ts)
This module is the only browser-visible auth token boundary. The web app keeps the short-lived access token in memory only. Refresh persistence lives in an HttpOnly cookie set by the API. Other modules must use the exported read/write/clear helpers.
Auth Transport (lib/client-api.ts, lib/axios.ts)
Browser auth uses a split model:
- Sign-in/sign-up return an access token in JSON and set an HttpOnly refresh cookie for browser-origin requests.
- Axios sends
withCredentials: true, so/v1/auth/refreshcan rotate the refresh cookie. - Session bootstrap restores the in-memory access token through
/v1/auth/refreshbefore protected routes decide whether to redirect.
URL State (lib/url-state.tsx)
Wraps the nuqs library. Direct nuqs imports are forbidden elsewhere. Direct window.location reads are also forbidden. Use the URL state wrappers instead.
Notifications (lib/notify.tsx)
Wraps the sonner toast library. Direct sonner imports are forbidden outside this module.
Environment (lib/env.ts)
All runtime environment reads are centralized here. Direct process.env access is forbidden in web source modules.
Optional Sentry Errors
The web app supports optional Sentry error reporting using NEXT_PUBLIC_SENTRY_DSN.
- If
NEXT_PUBLIC_SENTRY_DSNis blank/unset, Sentry is skipped. - Sentry is configured for errors only (
tracesSampleRate: 0). - Initialization runs from
AppProvidersthroughapps/web/lib/sentry.ts.
See Sentry (Optional) for complete setup.
API Client Generation
The web app uses Orval to generate typed API hooks from the OpenAPI spec:
# Regenerate after API changes
pnpm api:client:generateThis produces files under apps/web/lib/api/generated/. The generated client uses a custom Axios mutator defined in apps/web/lib/api/orval-mutator.ts that injects the in-memory access token and points to API_BASE_URL.
The Axios client in apps/web/lib/axios.ts must use webEnv.API_BASE_URL as its base URL. Proxy paths like /api/* are forbidden.
Integration Testing
Web integration tests are colocated alongside the components and pages they test. The suite covers auth flows, onboarding, multi-tenancy (orgs + workspaces), billing UX, media management, and access-gate components:
| Category | Representative test files |
|---|---|
| Auth | components/{AuthForm,SignOutButton,ForgotPasswordForm,ResetPasswordForm}.integration.test.tsx |
| Multi-tenancy | components/{CreateOrganizationForm,CreateInvitationForm,AcceptInvitationForm}.integration.test.tsx |
| Access gates | components/{PermissionsGate,SubscriptionGate,SuspensionBanner}.integration.test.tsx |
| Org + workspace pages | app/(application)/org/**/page.integration.test.tsx |
| Onboarding | app/(application)/org/onboarding/page.integration.test.tsx, components/onboarding/SpendCapStep.integration.test.tsx |
| Billing UX | components/billing/{AutoRechargeForm,BillingSettingsForm,CreditBalanceWidget,CreditHistoryTable,GracePeriodBanner,NoCapReminderCard,PlanSelector,RechargeRequiresActionBanner,SpendCapReminderCard,ThreeDSChallengeModal,TopUpDialog,UsageBreakdownCard}.integration.test.tsx |
| Billing pages | app/(application)/org/[orgId]/billing/**/page.integration.test.tsx |
| Media library | app/(application)/org/[orgId]/[teamId]/media/page.integration.test.tsx |
| Review flow | app/review/[token]/page.integration.test.tsx |
| Marketing | app/(website)/{page,LandingPageContent}.integration.test.tsx |
Run find apps/web -name '*.integration.test.tsx' for the canonical list.
Suite lifecycle is centralized in apps/web/vitest.integration.setup.ts. Individual test files must not call setupWebIntegrationSuite, resetWebIntegrationCase, or teardownWebIntegrationSuite directly.
Integration suites verify unauthenticated redirect behavior (e.g., /sign-in?next=%2Fdashboard), invalid-token handling (401 -> clear session -> redirect), and authenticated data-loading paths with seeded fixtures.