“We'll just share the password”
The shared account isn't a choice, it's what's left when the tool offers nothing else. It works fine for a few months, then it presents its bill, and always at the worst moment.
Three consequences, all mechanical. Removing access for one person forces you to change EVERYONE's password, so you re-brief a whole team on the day someone leaves — that is, the day you least want to deal with it. Next, nobody can answer “who changed this row?”, since everyone is the same person as far as the application is concerned. Finally, you can't grant partial access: someone who only comes to read a figure gets exactly the same powers as the person who invoices.
So the stake isn't security in the dramatic sense — nobody is attacking a fifteen-person application. It's irreversibility: a shared account can't be untangled, only replaced, and the longer you wait the heavier the replacement.
- Removing one person's access = changing everyone's password.
- No answer to “who did this?”: one account, one identity.
- No read-only access: viewing and editing carry the same rights.
Three roles, and the exact line between them
As soon as the application asks for a login, it ships real separate accounts and three roles. There are few of them on purpose: a permission system you can't grasp at a glance is a permission system you configure badly.
The administrator does everything, and is the only one who manages accounts: creating them, disabling them, resetting a password. The user works inside the application — creating, editing and deleting data — but never sees account management. The read-only role consults: it navigates everywhere, opens records, reads dashboards, and cannot write anything.
What matters isn't the list, it's where the refusal sits. For the read-only role, the block is not a button hidden on screen: every write request is refused before it reaches the route, by the middleware, with a 403. A hidden button can be worked around with a keyboard or a developer tool; a refusal placed upstream cannot. That's the difference between an interface that suggests and an application that holds.
The role travels in the signed session cookie, which is what allows this check without querying the database on every request. And a visitor without a session gets through nothing: pages send them to the login screen, remembering where they were headed, and API calls receive a 401.
- Administrator — everything, plus account management.
- User — creates, edits and deletes data; no access to accounts.
- Read-only — browses and consults; every write refused with a 403 before the route.
- No session — 401 on the API, redirect to login on pages.
Opening an account: a username, a role, a temporary password
Creating an account takes three pieces of information, and none of them is an email address. The administrator enters a username (2 to 31 characters: letters, digits, dot, hyphen, underscore), picks the role, and the application generates a temporary password itself.
That password is shown once, right then. It cannot be read back afterwards — not in the account list, not anywhere: only its hash is kept. If it's lost before being passed on, the administrator regenerates one, which takes ten seconds. The account is also flagged as needing a password change: the person must choose their own at first login, so the administrator does not know their colleagues' passwords.
No email is sent, and it's worth knowing before you organise around it: the application has no mail service at all. It is up to the administrator to pass on the username and temporary password by their own means. The corollary bites harder in practice than the missing email itself — there is no self-service “forgot my password”: whoever loses theirs goes through the administrator, who resets it.
The very first account is created with the application: username “admin”, password “admin”, and the same mandatory change at first login. That's a bootstrap password, not a password — and the application won't let you keep it.
- Username + role: that's all the administrator enters.
- Temporary password generated by the application, shown ONCE.
- Mandatory change at first login — the administrator doesn't know it afterwards.
- No email: passing it on and resetting it both go through the administrator.
Closing an account without punching a hole in the history
An account isn't deleted, it's disabled — and can be re-enabled. That isn't an implementation shortcut, it's the same judgement as the data trash: a record deleted for good takes everything attached to it along, and you only notice months later, when you look for something that no longer exists.
Two refusals are written into the code, and they're the ones that stop you locking yourself out. You cannot disable your own account — the easiest thing to do by accident while tidying a list. And you cannot disable the last active administrator: an application with no administrator is one where nobody can ever open an account again, including for themselves. The refusal is explicit; it doesn't merely fail.
So a departure is handled in one gesture, without touching anyone else's password and without erasing a trace. A replacement takes two: disable, then create.
- Reversible disabling rather than deletion.
- You cannot disable your own account.
- You cannot disable the last active administrator.
- Password reset by the administrator, at any time.
What it does not do, and you're better off knowing first
Permissions are global to the application. An account carries a role, and that role attaches to no entity, no section and no field. So there is no way to express “this salesperson only sees their own customers”, “this person can reach interventions but not invoicing”, or “this field is hidden from non-managers”. Three roles cover the question “view or edit” well, and the question “which part” not at all.
The application records WHEN data changed, never WHO changed it. Every record automatically carries its creation date and its last-modified date, but no column keeps the author. Separate accounts therefore answer “who may come in”, not “who wrote this row” — two different questions, and only the first is handled. It's the limit most easily mistaken for a shipped capability, because having named accounts feels like having an audit trail.
Logging in uses a username and a password, and nothing else. No sign-in with a Google or Microsoft account, no enterprise single sign-on, no second factor. For a team that already manages its identities elsewhere, that means one more list of accounts to maintain.
These four limits aren't oversights to work around: they are the edges of what is shipped, and writing them down beats letting you discover them. They all share one remedy, which is the whole point of the product — the application's code belongs to you. It's a standard Next.js and Prisma project, exportable as a ZIP or pushed to your own repository: adding an author column, per-team partitioning or enterprise authentication is ordinary development work, on code you own, that no vendor can refuse you. And if partitioning is structural to your business, describe it from the start — your text is what sets the entities and their links.
- Global permissions: no partitioning by entity, section or field.
- Modification date recorded, author not recorded.
- Username and password only: no SSO, no third-party login, no second factor.
- Shared remedy: the code is yours, and these extensions are ordinary development.