Sign-in
Sign-in gives your app accounts. Turn it on and visitors can create an account and sign in; turn it off and the app is open to everyone. It’s listed first here because it’s the one capability that changes how all the others behave.
Users are kept in the project’s database (Cloudflare D1) in your Cloudflare
account — the same <project>-db that Data records,
orders and received mail live in. There
is one database per project and it belongs to the project, not to sign-in. You own the
accounts, the emails, and the credentials, exactly as you own everything else Typillar
ships (see what you own). The sign-in and session handling are
generated into the app itself; there is no Typillar login sitting between your users and
your project.
The endpoints
Section titled “The endpoints”The pages and endpoints below are generated into the app and served from its own origin. You never write a login form, a users table, or a session cookie:
| Method & path | Body | Returns |
|---|---|---|
GET /login |
— | the sign-in page (styled to match) |
GET /signup |
— | the sign-up page |
GET /forgot |
— | the forgot-password page |
GET /account |
— | the account page |
GET /api/me |
— | the user {"id","email","name","provider","role","verified"} or null — currentUser() reads this |
POST /api/auth/logout |
— | clears the session — logout() posts this for you |
The ready-made pages post the credentials to /api/auth/* themselves — password, magic-link,
GitHub and Google. Your app links to /login and /signup, and asks who is signed in through
the auth import rather than a hand-written fetch. / is always your page.
import { currentUser, isStaff, logout } from "auth";
const user = await currentUser();currentUser() answers the user record itself or null — there is no wrapper around it. It is
read once and cached, so calling it in ten components costs one request; refreshUser() re-reads
it, isStaff() is the back-office check, and logout() ends the session and goes home.
The display name
Section titled “The display name”A person’s name belongs to their account, not to any one screen. Sign-up requires it,
/account changes it, GitHub and Google supply it, and GET /api/me hands it back. Set once,
it is the same everywhere the app shows them — including the roster of a
Realtime room, which is why no socket, frame or query string can override
it.
A magic link has no form to ask on, and a provider can hand over an account with no name on it.
Those accounts land on /account the one time they’re created, rather than on your /, so the
first thing their owner sees is the field. Until they answer, GET /api/me returns
name: null — render that case.
Styling the pages
Section titled “Styling the pages”The sign-in, sign-up, forgot-password and account pages are served by the platform rather than written into your files, so they’re skinned from a small set of theme tokens the agent sets to match the app it built — you never edit their HTML or CSS:
| Token | What it sets |
|---|---|
scheme |
dark or light — picks the whole neutral palette |
background |
the page background colour |
accent |
the primary button colour; readable text on it is derived automatically |
radius |
the corner radius, in pixels |
font |
the font-family stack |
logo |
a mark shown in place of the brand dot, from an image the app already ships |
The agent sets these the moment it turns sign-in on, and again whenever the app’s look changes, so the account pages keep matching the rest of the app. Every token has a default — a dark, monochrome look — and any value that doesn’t validate is dropped rather than applied, so the pages always render.
Verified addresses
Section titled “Verified addresses”Every account carries an honest flag: whether its email address has been proven — somebody read the mail there — or merely claimed by whoever typed it into a form. A setting on the capability decides how much that flag gates:
optional(the default) — sign-up creates the account and signs the person in straight away. If Email is set up, a verification link is mailed and the flag flips when it’s used; if not, the account simply stays unverified. Sign-up works from the first deploy, with nothing else configured.required— signing up does not create the account. It emails a confirmation link, and the account exists only once the link is used. Sign-in is refused until the address is proven. This mode needs Email turned on with a verified sending domain; without one, the sign-up page says sign-ups aren’t open yet and no account is created.
The agent picks a mode when it turns sign-in on — and asks you when the app makes it a real
decision — and you can change it any time in the project’s Auth tab. GET /api/me returns
"verified", so the app can nudge unverified users itself.
A proof always beats a claim. Magic links, Google and GitHub (whose addresses must be marked verified at the provider — an unverified provider email is refused), the sign-up confirmation link and the password-reset link are all proofs. When a proof arrives for an address that an unverified account merely claimed, that account is seized: whoever proved the address takes it over, and the claimant’s password and sessions are severed. An address someone else typed — by mistake or on purpose — can never keep its real owner out, and can never hand that impostor a way back in.
Forgotten passwords are reset at /forgot, by emailed link. Choosing a new password signs out
every other session.
What it changes
Section titled “What it changes”Sign-in isn’t a silo — it’s the identity every other capability reads.
It partitions data, per user, automatically. With sign-in on, each signed-in user gets their own private slice of every capability:
- their own records — one user can’t read another’s rows;
- their own files and optimized images;
- their own search index;
- their own live data stream;
- their own orders.
You never write this partitioning. The endpoints derive it from the signed-in user, so “only the owner can see it” is the default, not something you have to remember to enforce.
It gates the capabilities that cost money. Email sending requires a signed-in user (so your domain can’t become an open relay), and AI and search require one when sign-in is on (so an anonymous visitor can’t spend your inference budget).
Every account is a member until you make it staff in the project’s Auth tab. That switch is yours alone — the app has no endpoint for it, so nothing a visitor does, and no code the agent writes, can promote anybody.
Staff is the app’s back office in one flag:
- staff read every user’s records in a
usercollection, and can change or delete anyone’s record in apublicone; - a collection declared
"scope": "staff"is theirs alone — everyone else gets a403; GET /api/mereturns"role"— the app awaitsisStaff()from theauthimport, renders its admin screens behind it, and shows a member the door.
Nothing else changes: an admin list is the same query as an ordinary one, just unfiltered. An app can have no staff at all, which is the normal state until you appoint someone.
You can also work the data yourself, without the app: the console’s Data tab edits and deletes records in the live app directly, checked against the same collection rules the app writes through.
With sign-in off
Section titled “With sign-in off”Without sign-in the app has no user, and nothing falls open as a result: a signed-out
visitor is refused every collection until that collection opts them in with
anonymous, uploads always need an account,
and a scheduled job always needs a caller. An app with no sign-in therefore serves exactly
what it declared it would serve and nothing more. Turn sign-in on the moment the app holds
anything that belongs to a particular person — that is what gives each of them their own
records rather than one pool shared with every stranger.
How it’s built
Section titled “How it’s built”Users and sessions are private tables in the project’s one database (<project>-db), the
same D1 that Data uses for app records. Sign-in does not get a database
of its own, and turning it on does not expose Data’s /api/_data record API. It creates the
shared database in your Cloudflare account immediately — not at the next deploy — so the switch
never settles on a database that is not there.
The account, session, and sign-in flow are then generated directly into the app worker. It’s self-contained — the running app depends on no Typillar service to authenticate your users. See permissions & access for exactly what Typillar can and can’t touch in your account.