Skip to main content
Feather signs people in without passwords, in one of two ways:
  • Google. The user signs in with their Google account. You need OAuth credentials from a Google Cloud project.
  • Email. The user types their address and gets a link that signs them in. You need no Google Cloud project, only a way to send email.
Either way there is no password to store, and one flow covers both sign-up and sign-in: Feather creates the account the first time someone arrives. The approval workflows, roles and permissions below work the same for both. Choose with SIGN_IN_METHOD in config.py, or with feather new --sign-in email.

Setup: Google

1

Create OAuth credentials

2

Add the redirect URI

http://localhost:5173/auth/google/callback for development, or your production URL.
3

Add credentials to .env

.env
4

Create your admin user

Setup: email

1

Choose email sign-in

feather new --sign-in email sets it for a new app. In an existing one, set it in config.py:
config.py
2

Give it a way to send the email

Feather uses the first of these that is set:
3

Create your admin user

Then sign in with the admin address from seeds.py.

How a sign-in code works

  1. /auth/email/login asks for an address and emails a six-digit code. The page asks for the code whether or not the address has an account, so the form can’t be used to find out who does.
  2. The code page has a box per digit. Typing the last digit, pasting the code, or letting a phone fill it in from the email signs the person in straight away, with no button to press. Without JavaScript it’s one field and a Sign in button.
  3. New addresses are handled exactly as a first Google sign-in would be: created, and approved or held for approval.
A code works only in the browser that asked for it. That browser’s session holds a keyed hash of the code (never the code itself), the address and the expiry, so someone who sees or is forwarded the email has nowhere to use it. That is the point of a code over a link, which signs in whoever opens it. A code works once, expires after SIGN_IN_CODE_MINUTES (15), and stops working after five wrong tries; Send a new code replaces it. The tries and “works once” are counted in the cache, so with several workers configure Redis (REDIS_URL) to make them hold across all of them. To change how the pages look, copy feather/templates/auth/email_sign_in.html to templates/auth/email_sign_in.html in your app.

Routes

Only the routes for your SIGN_IN_METHOD answer; the others return 404. Pages behind @auth_required send signed-out visitors to the right one.

Approval workflows

In a single-tenant app, the address in ADMIN_EMAIL (in config.py, filled in from feather new --admin-email) is an active admin from its first sign-in, so the app’s owner is never held for approval. Everyone else follows the rules below. New users can be approved automatically or held for admin review. Manual approval creates users in a suspended state. They see a pending-approval page until an admin approves them in the admin panel. This keeps drive-by signups out and leaves you in control of who has access. Auto-approve activates users on first login. Choosing it during scaffolding sets AUTO_APPROVE_USERS = True in config.py, and the framework handles the rest. There are no callback files or environment variables to add.
To switch an existing app to auto-approve, add AUTO_APPROVE_USERS = True to config.py.

Status pages

Both pages are scaffolded with a message and a logout button, and use @login_only so users stay authenticated while they read them. Edit the templates in templates/pages/account/ to match your branding.

Decorators

Which one to use

@login_only does not check that the account is active. Use it only for the pending and suspended pages. On any other route a suspended account keeps working, so feather check reports it on API routes and on routes that change data as login-only-action.
| @role_required('editor') | Checking by role name, with inheritance | | @permission_required('resources.create') | Checking by action, which reads more clearly | | @admin_required | Shorthand for @role_required('admin') |

Roles

These defaults suit most apps, and you can add, remove or rename them. Roles inherit permissions, so @role_required('editor') admits editors and admins. To customize, edit the hierarchy in feather/auth/roles.py:
Then use @role_required('reviewer'). The User model’s role field is a plain string, so adding a role needs no migration.

Permissions

Access control by action, mapped onto roles.
Permissions live in feather/auth/permissions.py and extend the same way roles do.

Seeds

seeds.py populates initial data. The scaffolded version creates your admin user with the email you gave during feather new. Extend it for your own data:
seeds.py
Run it with python seeds.py or feather db seed. The scaffolded seed is idempotent, updating existing users rather than duplicating them.

Callbacks

For apps that need account setup after OAuth.
.env
myapp/auth.py
Use it to create Account or Membership records, assign tenants to users with public email addresses, or drive an onboarding flow.
Block new registrations before the account is created. This runs during signup for new users only. Existing users logging in are unaffected.
.env
myapp/auth.py
A returned string blocks registration, shows the message as a toast error, and creates no user record. None lets registration proceed. Errors raised inside the callback are logged but don’t block signups.