Skip to main content
Fymo owns no user model. Identity is just a uid string your app produces, and Fymo gives you the mechanism around it: resolvers, request-scoped caching, and guards for both routes and remote functions. There’s no User class, no built-in user table, and no config block that turns auth on. You write a resolver, and identity exists.

Identity resolvers

An identity resolver is a plain function: it takes a ResolverEvent and returns an Identity, or None for an anonymous request. Register one with @identify, and Fymo auto-discovers it from any file under app/auth/ (skipping __init__.py and files starting with an underscore, the same convention app/remote/ uses).
app/auth/resolver.py
Identity is a frozen dataclass with one field, uid. ResolverEvent carries remote_addr, cookies, headers, and scheme, a narrow read-only view of the request rather than the whole thing. Resolvers run in filename-sorted registration order, and the first one to return a non-None Identity wins. Read the resolved uid anywhere inside a request with current_uid(). It walks the resolver chain at most once per request and caches the result, including the anonymous outcome, so stacking several resolvers costs nothing extra.
Calling current_uid() outside a request raises RuntimeError rather than quietly returning None. A misplaced call fails loudly instead of looking like the visitor just isn’t signed in.
fymo.auth.current_uid() is not the same thing as fymo.remote.current_uid(). The one in fymo.remote reads the anonymous fymo_uid tracking cookie every request gets, signed in or not. The one in fymo.auth is the identity your resolvers produced. Import from the module you actually mean.
There’s no config key that turns identity on. Presence of any @identify resolver in app/auth/ is what does it, and deleting the whole directory turns it back off.

Protecting routes

Add require_auth: true to a route to redirect anonymous visitors before any server-side rendering starts. A route needs the dict form, with to:, once it carries any attribute:
fymo.yml
For a resources: entry, use a dict list item with name::
fymo.yml
By convention, the route named signin is the redirect target and is always public. Every protected app needs one; leaving it out is a boot-time ConfigurationError that names the fix directly. Verified against a live app: an anonymous request to a protected root gets 302 with Location: /signin?next=%2F. A request carrying a valid session cookie gets 200. The next value is only ever the original path and query, never a full URL, so the signin page can send a visitor back where they came from. require_auth also accepts a dotted path to a guard function instead of true:
fymo.yml
The path must resolve to a zero-argument callable, checked at boot so a typo fails before your app ever serves a request. Fymo calls the guard only after confirming the visitor is signed in; any exception it raises redirects the same way anonymous access does.
A route alias inherits the strictest require_auth declared anywhere for its controller, so hitting /home when only root is marked protected doesn’t bypass the guard.

Protecting remote functions

@require_auth wraps a remote function so a missing identity short-circuits into a 401 before the body runs:
app/remote/posts.py
It raises AuthRequired, a RemoteError the router already knows how to serialize as {type: "error", status: 401, error: "unauthenticated"}. If your app has a signin route, that envelope also carries a signin field. The generated client’s fetch wrapper follows it automatically, navigating there with ?next= instead of just throwing, the same redirect a protected page gives an anonymous visitor.

Identity extras

current_uid() only ever gives you a string. Anything else about the signed-in caller, an email, a role, an organization, is app data you attach with register_identity_extras_hook. The hook runs once per request scope, right after a resolver returns a uid, and its result is available for the rest of the request through identity_extras().
app/auth/extras.py
identity_extras() never raises for a missing hook or an unresolved identity, it just returns an empty mapping. It only raises the way current_uid() does, when called outside a request scope entirely.

The identity store

Extras stay on the server by default. What the browser sees is whatever your @public_identity projection returns, and nothing else. Register exactly one:
app/auth/public.py
Treat this as a whitelist, not a spread. Whatever the function returns is embedded in every page’s HTML for that signed-in visitor, so add fields one at a time and leave out anything sensitive. Register nothing, and a signed-in visitor sees the safe default, {"uid": ...} alone. The projection is exposed to Svelte as the identity store from $auth, hydrated at SSR and refreshed on every soft navigation:
The scaffolded identity store is enough for most apps. The reference blog app builds a richer client-side auth store on top of $remote/auth instead (see app/lib/auth.ts there), which is one valid way to grow past the default once you need more than a signed-in flag.

Generating auth

fymo new scaffolds working password auth by default: app/auth/, app/remote/auth.py, and a /signin page, ready to sign up and sign in against right away. Pass --no-auth to skip it and add auth later. fymo generate auth writes the identity code into an existing project. Two flags change what it generates: The signin page itself is the one piece fymo generate auth doesn’t write in an existing project (a fresh fymo new ships it). The command’s own next-steps say exactly what to add: a signin: signin.index route, a controller, and a form calling the login remote function. fymo generate page signin scaffolds the route and both files; the form body is yours to write. The --clerk resolver reads CLERK_ISSUER from the environment, or derives it from PUBLIC_CLERK_PUBLISHABLE_KEY when that’s unset, and verifies RS256 JWTs against {issuer}/.well-known/jwks.json. It namespaces the resolved uid as clerk:<sub>, so it can never collide with a uid from another mechanism. The bare (password) generator’s app/remote/auth.py gives you signup, login, logout, and me. Passwords are hashed with scrypt, a standard-library algorithm needing no extra dependency, bounded to 8-1024 characters with no character-class rules. Login returns one generic 401 for both an unknown email and a wrong password, so response timing can’t be used to guess which accounts exist.
Setting an auth: block in fymo.yml is a hard boot error now. Identity lives in code, in app/auth/, not in config. Run fymo generate auth and delete the auth: block.

Cookies

set_cookie and clear_cookie live in fymo.remote, not fymo.auth. They queue a Set-Cookie header for the response, drained by the router once your function returns, so you never touch the WSGI response directly:
These are the primitives the generated start_session/end_session helpers in app/auth/resolver.py wrap. Reach for them directly if you’re writing a resolver of your own.

Testing identities

fymo.testing.signed_in simulates an authenticated caller for a remote function called directly, bypassing the WSGI layer entirely. acting_as, nested inside it, swaps in a second identity mid-test, which is what makes it possible to prove one user can’t see or touch another’s data:
This is the real test from the reference blog app, and it passes against the actual identity chain. signed_in registers a resolver through the same @identify mechanism a live request uses, so there’s no separate mock path to drift out of sync with production behavior. For a deeper look at init_providers and testing storage, jobs, and broadcasts alongside identity, see Testing your app.