3 Accounts and invitations
Tiago Águeda edited this page 2026-09-19 10:43:37 +02:00

Accounts and invitations

Postulo is multi-user, and closed by default.

Who a person is

Every account has three things, all obligatory:

  • A username — 3 to 32 lowercase letters, digits, dots, underscores or hyphens. It is what you sign in with, and what other people on a shared instance see. Chosen at signup; changed under Settings → Account.
  • A full name — first and last. Every document starts from it, so Postulo asks for it up front rather than printing a CV with a blank at the top.
  • An email address — unique across the instance, and verified: a link is sent to it, and the account cannot sign in until the link has been followed. The address works for signing in too, interchangeably with the username.

An account may hold up to five addresses (Settings → Account → Email addresses), each of them verified by its own link. One is primary: it receives Postulo's mail and is the one an export records. An address cannot be made primary until it has been verified.

Accounts that existed before verification was required were marked verified when the instance was upgraded: they had been signing in by those addresses all along.

Your picture

The header shows your initials on a coloured tile until you give it a picture, under Your details → Your picture. Two ways, in this order of precedence:

  • Upload one. PNG, JPEG, WebP or GIF up to 5 MB. Postulo decodes it, straightens it, cuts it to a square and re-encodes it, which also strips everything the file knew about where and when it was taken — a phone photograph carries the place. It is stored under private media and served only to you (and to administrators), never by the web server.
  • Use my Gravatar. Tick the box and Postulo fetches the picture for your primary address from gravatar.com once, from the server, keeps a copy and shows that. Pages never point your browser at Gravatar: an image loaded from there would tell them your address and a hash of your email on every page view. It is fetched again when your primary address changes, or when you press Fetch my Gravatar again. Untick the box and the copy is deleted. If Gravatar has no picture for your address, the initials stay and the page says so.

Nothing here appears on a CV. Whether a photograph belongs on one depends on the country, and that will be a per-CV choice, off by default, when it arrives.

Two-factor authentication

Optional, per person, under Settings → Account → Two-factor authentication. Once it is on, signing in asks for a six-digit code from an authenticator app after the password, so a leaked password alone is not enough.

  • Setting up: scan the QR code with any authenticator app (Aegis, 2FAS, Ente Auth, Google Authenticator, the one built into your password manager), or type the key, then confirm with one code. Postulo asks for your password again first.

  • Recovery codes: ten single-use codes, shown when you set up and available again from the same page. Keep them somewhere that is not your phone; each signs you in once when the phone is not to hand.

  • Trust this browser: after a code, Postulo offers to skip the question on that browser for thirty days. Decline on a shared computer.

  • Lost the phone and the codes? Whoever has a shell on the server can remove the second factor from an account:

    uv run manage.py mfa_reset alex.morgan
    

    A password alone signs in again; set it up afresh afterwards.

API tokens are their own credential and do not go through the second factor: a browser extension has no code to type. See The capture API.

Passkeys

A passkey is held by your device or your password manager and released by a fingerprint, your face or a device PIN. It is the best way in Postulo has:

  • There is no shared secret. Nothing to leak in a breach, nothing to reuse on another site, and no code to read out to somebody who has phoned you pretending to be support.
  • It is already two factors — the device you have, unlocked by something you are or know — so it stands on its own rather than being a step after a password.
  • It cannot be used on the wrong site. The browser will only offer a passkey to the address it was made at, which is what makes it proof against a convincing copy of your sign-in page.

Add one under Settings → Account → Passkeys. Give it a name you will recognise later, and leave Let this passkey sign me in on its own ticked unless you want it only as a second step after your password. Then Sign in with a passkey on the sign-in page is all it takes.

Two things are worth knowing before you rely on one.

It is tied to the address you made it at. A passkey made at postulo.example.org will not work at jobs.example.org, and moving your instance to a new name makes every existing passkey unusable there. That is the standard, not a limitation of Postulo, and there is no migrating them: add new ones at the new address and remove the old. The account page names the address you are currently on, so you can see what you are committing to.

Your browser needs HTTPS. Over plain HTTP browsers refuse the whole mechanism, with localhost the only exception. An instance reached at a bare address over a mesh VPN cannot offer passkeys at all, and no server setting changes that; the account page says so instead of showing a button that would fail.

Make recovery codes. Once a passkey is your only way in, losing the device means losing the account. Recovery codes are the answer, they live under Two-factor authentication, and the account page nags you until you have some. Keep them somewhere that is not the device.

Single sign-on

An instance can sign people in through an OpenID Connect identity provider — Authentik, Keycloak, Pocket ID, Zitadel, Kanidm, Google, or anything else that speaks it. It is native: nothing to install, four variables in the environment (see Configuration), and a button on the sign-in page named after the provider.

  • Existing accounts link, they are not duplicated. An address the provider has verified signs in the account that holds it, and the provider is connected to that account from then on. People can see and disconnect it under Settings → Account → Sign-in methods. Only a verified address ever matches; an unverified claim links to nothing. That does mean the instance takes your provider's word for what verified means, so if you do not run the provider, read the note on Hardening and consider POSTULO_OIDC_LINK_BY_EMAIL=false, which makes each person connect the provider from their own account page instead.
  • By default, existing accounts only. The identity provider is not an invitation: someone it knows but Postulo does not is turned away, unless they followed an invitation link or registration is open. Set POSTULO_OIDC_AUTO_SIGNUP=true to let the provider create accounts — right for a household or a small team where it already gates everything.
  • The username and name come from the provider's claims — preferred_username, given_name, family_name — bent to Postulo's rules, with the address's local part as the fallback for the username. An address the provider says it verified needs no further click.
  • Two-factor still applies after a single sign-on, if the person has it on.
  • The callback address the provider must be told is shown under Server settings → Sign-in, exactly as the browser reaches it: scheme, host and port must match.

Sign-in tokens from the provider are not stored; Postulo has no use for them.

Administrators

An administrator runs the instance: invites people, sees the list of accounts, and changes the instance's policy under Server settings, reached from the account menu (top right). Administrators see accounts — never anyone's applications, documents or contacts; those stay private to the person who owns them.

The first account is the administrator. On an empty instance the sign-up form is offered to whoever reaches it, and the account it creates administers the instance; its address is trusted, since nobody else exists yet to send a verification link. After that the door closes again. createsuperuser on the command line still works, and does the same thing.

Server settings → People lists every account and lets an administrator make or unmake other administrators, deactivate accounts (nothing deleted, no sign-in), delete an account outright (see Deleting your account) and change a person's username on their behalf. The last administrator cannot be removed, deactivated or deleted — by anyone, including themselves — and nobody can deactivate or delete the account they are signed in with from that page. A username is unique across the instance whoever changes it: a name already in use, in any capitalisation, is refused before anything is saved.

Each row's menu also holds Plugins for this account — the exceptions to what the instance decides for everybody. An administrator may make a plugin available, unavailable, always on or always off for one person, and whatever they decide is shown to that person on their own settings page with the administrator's name against it. Switching a plugin off stops it being used and deletes nothing: connections and their settings stay exactly as they are, and turning it back on brings them back unchanged.

Each row's actions live behind the ⋮ button at its end. They used to sit across the row with their names spelled out, which made that one column a third of the table and pushed the whole thing off the side of the page at every window size. On a phone the rows stop being rows at all and become a card each, with every value labelled by the column it came from.

Who can sign up

Registration is closed by default: the sign-up form is not offered, and the only way in is an invitation. An instance holding somebody's employment history has no reason to accept strangers.

An administrator opens it under Server settings → Sign-in. The operator may also pin it with POSTULO_REGISTRATION_OPEN in the environment, in which case the environment wins and the page says so. Open it only if you genuinely want anyone who finds the URL to be able to create an account.

Invitations

Server settings → People → Invitations.

An invitation is:

  • single use — it is spent the moment somebody signs up with it;
  • self-expiring — fourteen days by default;
  • optionally bound to one email address — and that binding is enforced at signup, not merely suggested, so an invitation addressed to one person cannot be redeemed by whoever else ends up holding the link. An invitation bound to an address also counts as that address's verification: the link went to that mailbox, and following it is the same proof a verification link would give, so the invited person is not asked twice.

Create one, copy the link from the page that confirms it, and send it however you like. The link is shown once, there. Postulo keeps only a fingerprint of it, as it does for a recovery link and an API token, so the list of invitations cannot show it again and a copy of the database is not a set of working invitations. Lost the link before sending it? Revoke the invitation and make another. Pending invitations can be revoked. Accepted ones cannot be deleted, because they are part of the record of who was let in.

Only administrators can issue invitations. To make someone an administrator, use Server settings → People.

Deleting your account

Settings → Your data → Delete my account. The page says exactly what goes — applications, listings, companies and contacts, documents and the files behind them on the disk, reminders, interviews, tokens, connections and their secrets, the invitations you issued that nobody accepted, the account itself — and offers the export first, prominently. Confirming takes your password again (or your second factor) and your address typed out. Then it is done, at once: there is no grace period, because a "pending deletion" state is a second thing to get wrong, and the export is the safety net.

The one account that cannot be deleted is the last administrator's, by anyone, including themselves: make somebody else an administrator first. An administrator can delete another person's account from Server settings → People (the same service, the same file cleanup), and an operator from the command line:

uv run manage.py delete_account alex.morgan     # asks first; --yes to skip the question

Separation between accounts

Every record in Postulo belongs to exactly one person, and every query is scoped to its owner. Companies, postings, applications, tags, CVs, letters and files are all private to the account that created them. Two people on the same instance can each track the same employer without ever seeing the other's notes.

Asking for a record belonging to someone else returns not found rather than forbidden — confirming that a record exists would itself disclose something.

The administration interface

Everything an administrator needs day to day is under Server settings: an overview of what is running and where the data is, the accounts, the sign-in policy, a test of the email settings, the installed plugins, capture policy, and the instance's name and the defaults new accounts start with.

Django's own admin is the escape hatch for anything Server settings does not cover, and it is off unless you ask for it. Give it a path and it appears there:

POSTULO_ADMIN_URL=some-private-path/

The path is not a security control on its own; it keeps the noise down, and the login is rate-limited either way. See Hardening for why it is off by default.

Passwords

A new password must be at least twelve characters, not all digits, not on the list of commonly used passwords, and not too close to your own name or address. Wherever you choose one — sign-up, an invitation, Change password, Set password for an account that came in through single sign-on, a reset link — a meter under the field says how strong it is as you type: very weak to strong, with a hint when it is low. The estimate is made in your browser (by zxcvbn, which knows about dictionary words, keyboard walks, dates and repeats, and about the name and address you typed above) and the password never leaves your browser until you submit the form. The rules listed under the field are what the server checks; the meter cannot contradict them. Nothing appears on the sign-in form.

Password resets are sent by email, so they need working email settings — see Configuration. Without them, reset a password from the command line:

uv run manage.py changepassword alex.morgan     # the username, not the address

Getting somebody back in without email

Until now the only way back into an account was a password-reset email, which made mail something every instance had to keep working for ever. An administrator can now issue a recovery link instead: Server settings → People → ⋯ → Recovery link.

It needs no third party, no gateway and no cost. You make a link, and you hand it over yourself — in person, on the telephone, through whatever channel you would already use to prove it was you. Postulo does not send it anywhere, and that is the point: sending it over the channel this exists to replace would be absurd.

The link is shown once. Nothing stores it — the row keeps only a fingerprint — so leaving the page is the last chance to copy it. If you lose it, make another; the old one stops working the moment you do.

It sets a password. It does not sign anybody in. They still sign in afterwards, and still give their second factor if they have one. A link that goes astray is a password change on an account whose other factors are untouched, not a session.

It lasts an hour and works once. Opening it does not spend it — a link-preview bot in the chat you sent it through would otherwise burn it — but choosing a password does.

Every one is recorded, and the record survives the link: who issued it, for whom, when, and whether it was used, revoked or left to expire. Taking somebody's account back is not something anybody should be able to do without a trace.

You cannot issue one for yourself in any useful sense. Making a link needs signing in, and the person who has forgotten their password cannot. So an administrator is covered by whatever else covers them: a passkey, or a second administrator. On an instance with one administrator and no passkey, that administrator is the one account this route does not reach — and Postulo says so on the page and keeps the mail lock shut accordingly.

What this changes about switching mail off

With a route that reaches everybody, Server settings → Email stops refusing to let the mail transport go. That covers getting existing people back in, and nothing else. A new account still has to verify an address before it exists, so an instance with mail switched off can recover the people it has and cannot admit new ones. The Email page says so where you would act on it, rather than leaving you to find out when the first sign-up fails.

Signing in with a code sent by email

Server settings → Sign-in can offer a fourth way in: somebody who has forgotten their password asks for a code at their primary address and types it back.

A code, not a link, and the reason is practical rather than theoretical. A link is a bearer credential that works from anywhere, and corporate mail scanners follow links — Defender, Proofpoint and their kind fetch every URL in a message, which spends a single-use link before the recipient has read the mail. Postulo's people correspond with recruiters, so some of them have exactly that kind of mail. A code typed back into the browser that asked for it cannot be burned by a scanner, cannot be usefully forwarded, and cannot be used from a device that is not the one signing in.

It is never a second factor. Somebody with an authenticator app is still asked for it after the code. There is no setting to change that, and that is where this deliberately parts company with single sign-on counts as the second factor: that setting exists because an identity provider may itself have checked identity carefully and Postulo cannot see how. A code out of an inbox has no provider behind it to trust, so there is nothing for an operator to decide and no switch to leave in the wrong position.

It is only offered while mail actually works. Not while it is configured — while it is delivering. Offering sign-in by email on an instance whose relay is broken is a page promising something it cannot do, to somebody who may have no other way in.

It goes to the primary address, so changing which address is primary moves it. Settings → Account says so beside the addresses rather than leaving you to find out.

The code lasts three minutes, three wrong guesses end the attempt, and three resends are the most one request will send — each resend is an email somebody may not have asked for. POSTULO_EMAIL_CODE_TIMEOUT, POSTULO_EMAIL_CODE_ATTEMPTS and POSTULO_EMAIL_CODE_RESENDS change those. The browser is never remembered: a one-off code must not become a standing credential on a machine that may not be yours next week.

It makes the mailbox more load-bearing, and that is worth saying plainly. Anybody who holds the mailbox holds the account, without needing to complete a password reset. That was nearly true already — a reset goes to the same place — but with this it is one step shorter.