Architecture

Dashboard internals

How apps/dashboard is built, for contributors. Routes, data access, credentials, domains and team.

apps/dashboard is the merchant admin application. For what merchants see, read the Dashboard section.

Port: 3002 · Framework: Next.js 16 · Auth: Better Auth + organization plugin

Architecture

Drag to pan · Scroll to zoom

Most data operations go through the Commerce API. Integration credentials and domain records are managed directly via Drizzle.

Routes

SectionRouteNotes
Overview/Stats and setup checklist
Orders/orders, /orders/[id]Server actions in app/(dashboard)/orders/actions.ts
Products/products, /products/new, /products/[id]/editcomponents/store/product-form.tsx. Writes go to /v1/admin/products
Customers/customers, /customers/[id]
Domains/domainsapp/(dashboard)/domains/actions.ts, lib/vercel.ts
Integrations/integrations, /integrations/[provider]Form built from lib/providers.ts
API keys/settings/api-keysStore owners and admins only
Team/teamBetter Auth organization plugin
Store settings/settings
Billing/billingShows the current plan

The sidebar lives in lib/nav.ts.

Auth and organizations

Merchants sign in with email and password. Better Auth's organization plugin provides multi-store support:

  • Registration creates a user, a first organization and an owner member
  • session.activeOrganizationId decides which store is shown
  • lib/create-store.ts uses the store address from the form. If it's taken or reserved, it adds a five-character random suffix, so the address isn't guaranteed to be what was typed
  • All admin API calls are scoped to the active organization
ts
// lib/admin.ts
export async function withActiveOrg<T>(fn: (orgId: string) => Promise<T>) {
  const orgId = await requireActiveOrg()
  return withTenant(orgId, fn)
}

Roles are checked by the API on every request. Owners and admins get admin, staff and storefront. Other members get staff and storefront. See Authentication.

Configuration

VariablePurpose
DATABASE_URLAuth, integration_config and tenant_domain tables
COMMERCE_API_URLAdmin API client base URL
BETTER_AUTH_SECRETSession validation on SSR (same secret as API)
NEXT_PUBLIC_AUTH_URLBrowser auth client → apps/api (default http://localhost:3005)
INTEGRATION_ENCRYPTION_KEYEncrypts provider credentials at rest
CHECKOUT_URLBuilds the webhook URL shown on a provider's page
VERCEL_TOKEN, STOREFRONT_VERCEL_PROJECT_ID, VERCEL_TEAM_IDCustom domain provisioning
NEXT_PUBLIC_PLATFORM_DOMAINShown on the domains page

Credentials

Payment credentials exist only per store. There is no environment fallback.

Drag to pan · Scroll to zoom

PAYMENT_PROVIDER_IDS in @prood/commerce lists providers in checkout preference order: Stripe, Easypay, Ifthenpay. A session uses the first one the store has connected and turned on.

Security:

  • Credentials are encrypted with AES-256-GCM (enc:v1: prefix) using INTEGRATION_ENCRYPTION_KEY, one key for the platform
  • The dashboard never sends raw secrets to the client
  • The commerce layer decrypts when building a provider at runtime

Domains

lib/vercel.ts wraps the Vercel SDK. Adding a domain inserts a tenant_domain row, adds the domain to the storefront's Vercel project and returns the DNS records the merchant must configure (lib/dns-records.ts). Once Vercel verifies it, the storefront resolves the organization from that host and Vercel provisions the certificate.

Limits are enforced in addDomainAction.

VariablePurpose
VERCEL_TOKENVercel API bearer token
STOREFRONT_VERCEL_PROJECT_IDStorefront Vercel project ID (store custom domains attach here)
VERCEL_TEAM_IDVercel team ID, if applicable

tenantDomain is defined in packages/auth/src/schema.ts. Subdomain resolution happens in apps/storefront/lib/tenant.ts. Locally, open http://prood-demo.localhost:3000 with NEXT_PUBLIC_PLATFORM_DOMAIN=localhost.