Web
The app your users sign in to, with the dashboard, the account, the organization and billing.
apps/web is the app your users sign in to. It holds the sign-up and sign-in
screens. It also holds the dashboard, and under it the account, the
organization and billing. You run it on app.<domain>.
This is the only app in the project that carries a user session. A session is the proof that a user has signed in.
The app is built with TanStack Start and runs on Cloudflare Workers by default. Vercel is supported too; see
Deployment. A Worker
is a small server that Cloudflare runs close to your users. The pages live in
src/routes. The name of a file there decides its URL.
Running it
pnpm run dev:web
This starts the app on port 3000. Open http://localhost:3000.
pnpm run env:web
This prints the environment the app loads. An environment variable is a value you set outside the code, like a password or a URL. Run this command first when the app stops and says a variable is missing.
The app needs a Postgres database running. Postgres is the database that stores
your users. Every protected page reads the session from it. See
Local development for the docker compose up that
starts Postgres and the rest.
The pages
Every page under _protected needs a session. The file
src/routes/_protected.tsx reads the session on the server. If there is none,
it sends the visitor to /sign-in.
export const Route = createFileRoute("/_protected")({
beforeLoad: async () => {
const session = await getSession();
if (!session) {
throw redirect({ to: "/sign-in" });
}
return { user: session.user, organizationId: session.session.activeOrganizationId ?? null };
},
component: ProtectedLayout,
});
What it returns is passed down to the pages inside. So a page can read the user without asking the server again.
These are the protected pages:
/onboarding— the name of the user, and the name of their first organization./dashboard— the first page after sign-in./dashboard/billing— the plan, the checkout and the customer portal./dashboard/organization— members, invitations and organization settings./dashboard/account— profile, email, password, passkeys, two-factor, sessions and API keys./dashboard/settings— theme and reduced motion.
These pages are open to everyone: /sign-in, /sign-up, /forgot-password,
/reset-password, /two-factor and /accept-invitation/$invitationId. The
page at / sends you straight to /dashboard.
The file src/routes/api.auth.$.ts handles sign-in and sign-up requests. The
browser calls /api/auth/* on the same address as the app.
Search engines are told to stay away. The app serves a robots.txt with
Disallow: /. Its pages also carry noindex, nofollow. Nothing here is meant
for a search engine.
What it uses from the project
The package.json of the app lists these. Each one has its own page:
@zap-ts/authentication— sign-in, sign-up and sessions@zap-ts/authorization— who may do what@zap-ts/billing— plans and subscriptions@zap-ts/database— the tables behind all of it@zap-ts/ui— the buttons, tables and colors on the page@zap-ts/tanstack-start— shared page and form pieces@zap-ts/ai,@zap-ts/mail,@zap-ts/analytics,@zap-ts/observability,@zap-ts/queues-cloudflare
The app also depends on api, but only for its types. The file
src/lib/api.ts builds a typed client from AppType. If you change a route in
apps/api, the build here fails. That is on purpose. It tells you early.
Where the database address comes from
One file decides: src/lib/database-url.ts. On Cloudflare it reads the
Hyperdrive binding. Hyperdrive is a Cloudflare service that sits in front of
Postgres and makes queries faster.
export const databaseUrl = (): string => cloudflareEnv.HYPERDRIVE.connectionString;
The binding is set in wrangler.jsonc. The id that ships is a demo one.
Replace it with the id of your own database before you deploy.
Languages
The app speaks English and French. It uses Paraglide, a tool that turns text files into typed functions.
The file project.inlang/settings.json lists the languages. English is the
base. The text lives in messages/en.json and messages/fr.json.
Run pnpm run i18n:compile to build the translations. The build and
typecheck scripts already run it first. So a missing translation stops the
build instead of showing an empty label.
To add a language, do two things:
- Add it to
localesinproject.inlang/settings.json. - Create
messages/<locale>.jsonwith the same keys.
The worker file
src/server.ts exports two things.
The first answers web requests. It wraps the app so each request arrives with the right language.
The second handles jobs from a queue. A queue holds work that runs later, after
the user already got their answer. The queue is set up in wrangler.jsonc. The
job handler that ships only writes a log line. Replace runJob with your own
work.
There is also src/sw.ts, a service worker. A service worker is a script the
browser keeps running in the background. This one saves pages, so the app still
opens when the network fails.
The end-to-end test
pnpm run test:e2e
The file e2e/happy-path.spec.ts runs one full path. It signs up, names an
organization, subscribes, and lands on the dashboard. It runs in Chromium,
Firefox and WebKit.
The test drives a real dev server. So it needs a Postgres address in
TEST_DATABASE_URL. The test never pays a real provider. It answers the
checkout call itself, with the same redirect the provider would send back.
What to change first
SITE_NAMEinsrc/lib/site.ts.public/manifest.webmanifest— the name, the description, the colors and the icons.- The Hyperdrive
idinwrangler.jsonc. - The plan ids, which live in
@zap-ts/billing, not here.
Deploying
pnpm run deploy:web:cloudflare
pnpm run deploy:web:vercel
The host is part of the name, so a deploy never goes somewhere you did not
name. There is no deploy on its own.
The Cloudflare one runs varlock-wrangler deploy. It sends your environment
variables up with the app. You do not set them by hand in the Cloudflare
dashboard. The Vercel one runs varlock run -- vercel deploy --prod, and there
you set the variables yourself. See Deployment.
The app declares no variables of its own. Its .env.schema takes the whole
root .env.schema, and adds API_URL from apps/api.
Related
- Marketing — the public site that links here
- Admin — the back office, with its own sign-in
- Authentication — the session this app reads
- Environment — how varlock loads
.env.schema - Going to production