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 aResolverEvent 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.@identify resolver in app/auth/ is what does it, and deleting the whole directory turns it back off.
Protecting routes
Addrequire_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
resources: entry, use a dict list item with name::
fymo.yml
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
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
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
{"uid": ...} alone.
The projection is exposed to Svelte as the identity store from $auth, hydrated at SSR and refreshed on every soft navigation:
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.
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:
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:
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.