Summary#
Adds an account to your app on the local platform: a row in your app's own user entity, with a hashed password and
the roles you asked for. It is how you get something to log in as, and how you create the user that --as names.
This is the way to seed an app's accounts — including the several users in different roles a test or a demo needs
in order to show that different people see different things. The remote twin is osyrin app user add: same act, same
store, same flags.
The account is your app's, not the platform's. The login field, the password field and the role vocabulary all come from what your app declares — so the account you add is exactly the kind of account your app's own login page produces, and it is subject to the same rules afterwards.
Signature#
osy user add <login> [path] --role <name>… [--set <field>=<value>]… [--password <pw>] [--via-signup] [--devname <name>]Description#
What it writes#
Your app declares its login this way:
[Principal] entity User {
[Required, MaxLength(200), Unique] string Email;
[MaxLength(200)] [Required] string PasswordHash;
}
app.Auth = new PasswordAuth { LoginField = Email, PasswordField = PasswordHash };osy user add ops@example.com writes one User row: Email set to the login you gave, PasswordHash set to a
hash of the password. Nothing is hardcoded — rename the entity or the fields and the command follows, because it
reads the same declaration your login page does.
Omit --password and you are prompted for it, and the prompt does not echo.
Giving it roles#
An account you add by hand usually exists because it needs authority, so --role is required — once per role:
$ osy user add ops@example.com --password 's3cret' --role Admin --role Support
✓ Added ops@example.com to Acme Ops with Admin, Support.The names come from your app's own role vocabulary, and the grant lands in the entity that pairs a user with it:
[Principal] entity User {
[Required, MaxLength(200), Unique] string Email;
[MaxLength(200)] [Required] string PasswordHash;
}
[Role] enum AppRole { Admin, Support }
entity RoleGrant {
[Required] User User;
[Required] AppRole Role;
}
app.Auth = new PasswordAuth { LoginField = Email, PasswordField = PasswordHash };A role your app cannot grant refuses the whole command, and says which of the three things is missing: no [Role]
enum at all, no entity pairing a user with one, or a name that is not a member of the enum — that last one with a
suggestion:
$ osy user add ops@example.com --role Admn
✗ 'Admn' is not a member of this application's `[Role]` enum AppRole. Did you mean 'Admin'?Nothing is written when it refuses. An account created with fewer rights than you asked for is worse than none: it logs in, and then fails at the first page that matters, in a way that reads like a bug in your app.
Omitting --role is an error, not a default. An account with no role holds nothing your app's security {}
can act on, and a missing flag is far more often forgotten than meant:
$ osy user add ops@example.com
✗ `--role` is required: an account with no role holds no authority this app's `security {}` can act on… Pass
`--role <name>` (repeatable) naming a member of the app's `[Role]` enum — or `--via-signup` to run the app's own
signup instead, which decides its own grants.If your app declares more than one entity that grants roles — say a global one beside a per-project one — a role name on its own does not say which grant to write, and the command says so and names the candidates. Create the row you meant directly.
Setting the other fields on the account#
The login and the password are the only fields the command knows by itself — they are the two your app.Auth names.
Everything else your account entity holds, you say:
$ osy user add lena@acme.test --password 's3cret' --role Member \
--set Name=Lena --set Department=Legal --set Seniority=4
✓ Added lena@acme.test to Acme Ops with Member.--set is repeatable, once per field. Values are written in the type the field declares, using the same rules a
data file uses — so an enum is written by member name (Department=Legal, not a number),
a number may be typed as text, and a true/false lands as a boolean. Only the first = splits, so a value may
contain more of them.
A field it cannot honour refuses the whole command, and nothing is written:
$ osy user add lena@acme.test --role Member --set Nmae=Lena
✗ 'User' has no property 'Nmae'. Did you mean 'Name'?
$ osy user add lena@acme.test --role Member --set Department=Marketing
✗ 'Department' is a Dept, which has no member 'Marketing'. It declares: Legal, Finance.That is the point of refusing rather than skipping: an account created with a field silently empty looks exactly like one where the model has no such field, and you find out later, from a blank column.
⚠ You cannot --set the login or the password field. The login comes from the argument, so passing it too would
be two answers to one question; and the password field holds a hash, so setting it as text would write a value no
login could ever verify. Use --password, which hashes it.
⚑ A required field is a required field. If your account entity requires something beyond the credential — a name,
a department — the command refuses until you --set it, and says so. It does not invent a value.
When your app's signup owns its authorization#
Some apps decide authority inside their own signup: the first account to register writes itself an admin grant, and the app's bootstrap gate is "no users exist yet". For those, an account added generically logs in and is then refused every admin page — and the row it wrote has closed the bootstrap gate for good.
--via-signup runs your app's own declared signup instead of writing the row:
$ osy user add founder@example.com --via-signup
✓ Ran Acme Ops's own signup for founder@example.com.What that grants is entirely your app's decision — which is the point of routing through its function rather than
imitating it. --role cannot be combined with --via-signup for the same reason: the signup decides what it grants,
so there is nowhere for a role you named to go.
The first account closes a Count() == 0 bootstrap gate#
This belongs to the act of adding an account, not to any one way of doing it. If your app gates its first-admin path
on User.Count() == 0, that gate is closed by the first row added by any means — this command, osyrin app user add, osy import, a browser signup. If you want the app's own bootstrap to run, run it first (--via-signup, or
the signup page) and seed the rest afterwards.
The account it makes is a real one#
There is no back door here. The row goes through your app's own declared fields, the password is hashed the same way
a login checks it, and the roles are the app's own. So the account works in the browser, in osy run --as, and in
osy import --as identically — and an account that cannot do something is telling you your app's rules say so.
Examples#
osy user add me@example.com --role Admin # prompt for the password
osy user add ops@example.com --password 's3cret' --role Admin # the privileged user `--as` names
osy user add lee@example.com --role Legal --role Support # repeat for several roles
osy user add lena@example.com --role Member --set Name=Lena --set Department=Legal # the rest of the person
osy user add founder@example.com --via-signup # let the app's own signup decide
osy user add me@example.com --role Admin --devname staging # against a named local instanceAgainst a deployed app#
osyrin app user add is the same command against a platform you are logged in to — same act, same store (your app's
own account rows), same flags, same refusals. The two are one verb with two reaches, so anything above holds there.
See also#
Running a function — osy run --as names the account you just added.
Importing data — osy import --as does too, for anything the app gates.
Launching your app — open the app in a browser and log in as it.