tx-agent-kit
Apps

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:

PagePathDescription
Landing/Config-driven hero, features grid, FAQ, CTA section with JSON-LD structured data
Blog listing/blogBlog articles with category filtering, skeleton loading, empty state
Blog post/blog/[slug]Individual article with author info, tags, hero image
Pricing/pricing3-tier pricing (Free/Pro/Enterprise) with FAQ section
Terms/termsTerms of Service (config-driven company references)
Privacy/privacyPrivacy 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:

PagePathDescription
Org redirect/orgSmart redirect: resolves first org → first team → dashboard
Workspaces/org/[orgId]/workspacesTeam 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

FilePurpose
lib/seo.tsbuildTitle, buildDescription, Organization/WebPage/BreadcrumbList/FAQPage structured data builders
lib/blog.tsBackend-agnostic BlogDataSource interface, estimateReadingTime, escapeXml
lib/blog-seo.tsBlogPosting and Blog JSON-LD structured data builders
components/Breadcrumbs.tsxBreadcrumb navigation with chevron separators and aria-label
components/StructuredData.tsxJSON-LD <script type="application/ld+json"> renderer
app/sitemap.tsSitemap with all marketing page URLs
app/robots.tsRobots.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:

ConstraintDetail
No server-side codeapp/api routes, proxy.ts, middleware.ts, next/server, and next/headers are all forbidden
Client-only componentsEvery .tsx file under app/ and components/ must start with 'use client'
No Effect or DrizzleThe web app must not import effect, effect/*, or drizzle-orm. Keep runtime complexity in the API/core/worker layers
No direct fetchAll 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/refresh can rotate the refresh cookie.
  • Session bootstrap restores the in-memory access token through /v1/auth/refresh before 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_DSN is blank/unset, Sentry is skipped.
  • Sentry is configured for errors only (tracesSampleRate: 0).
  • Initialization runs from AppProviders through apps/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:generate

This 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:

CategoryRepresentative test files
Authcomponents/{AuthForm,SignOutButton,ForgotPasswordForm,ResetPasswordForm}.integration.test.tsx
Multi-tenancycomponents/{CreateOrganizationForm,CreateInvitationForm,AcceptInvitationForm}.integration.test.tsx
Access gatescomponents/{PermissionsGate,SubscriptionGate,SuspensionBanner}.integration.test.tsx
Org + workspace pagesapp/(application)/org/**/page.integration.test.tsx
Onboardingapp/(application)/org/onboarding/page.integration.test.tsx, components/onboarding/SpendCapStep.integration.test.tsx
Billing UXcomponents/billing/{AutoRechargeForm,BillingSettingsForm,CreditBalanceWidget,CreditHistoryTable,GracePeriodBanner,NoCapReminderCard,PlanSelector,RechargeRequiresActionBanner,SpendCapReminderCard,ThreeDSChallengeModal,TopUpDialog,UsageBreakdownCard}.integration.test.tsx
Billing pagesapp/(application)/org/[orgId]/billing/**/page.integration.test.tsx
Media libraryapp/(application)/org/[orgId]/[teamId]/media/page.integration.test.tsx
Review flowapp/review/[token]/page.integration.test.tsx
Marketingapp/(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.

On this page