Security and access
Your app has no sign-in code in it. Spryloom decides who is at the door and tells the app who arrived.
That is the whole arrangement, and everything below follows from it.
Choosing who
One line in spryloom.yaml:
access:
visibility: invited
visibility |
Who can open it |
|---|---|
private |
You, and anybody in admins. Nobody can be invited to it. |
invited |
People you invited by name, and nobody else. |
company |
Anybody with an email address at domain. |
link |
Anybody who has the address, once they have signed in as somebody. |
company needs domain. The others do not.
A new app is private, which is the shut position: an app is closed until you
say who it is for. spry invite on a private app is refused, because an
invitation it could never honour is worse than none.
company needs a company workspace, and domain must be that workspace's
own domain. In a personal workspace, share with invited instead. How
workspaces are decided, and who sees which apps, is on
workspaces and sharing.
Change it and publish again; it takes effect immediately.
Inviting people
spry invite expense-notes --email ryan@yourcompany.com --email sam@yourcompany.com
They get an email naming the app, what it does, and who invited them, with an Open button. Beneath the button it says which address the button opens, in plain text. The button signs them in: they see "Continue to app as their address", press Continue, and are in. It works once, for 7 days. After that, or on another device, they sign in with an emailed link as usual. Somebody who cannot use the app is not invited and is not emailed, and you are told:
Invited 1 person.
They'll get an email with a button that signs them in.
Not invited:
someone@gmail.com — not at yourcompany.com
Inviting an address that is already on the app's invitation list sends them a
fresh invitation, and their earlier link stops working. That is how you replace
an invitation that expired or went astray; they are still listed once. For a company app, people at the configured domain already qualify
without an invitation. For a link app, sharing the app address is enough;
sign-in is still required.
How people get in
They open the app's address and are asked for their work email. Spryloom sends a link. They click it and they are in, on that app.
No passwords. Nothing for you to build, reset, or store.
The emailed link expires after 15 minutes and can be used once. Opening it
creates a session that lasts up to 30 days. The session is stored in a
Secure, HttpOnly, SameSite=Lax cookie on that app's exact hostname, so
browser JavaScript cannot read it and a sibling app does not receive it.
A session is for one app. Signing in to one grants nothing on another, even for the same person: the two are separate sessions on separate addresses. That is deliberate. An app somebody wrote in an afternoon should not be able to reach anything else your company runs.
Google sign-in is not available yet. signin: [google] is accepted in a manifest
and does nothing, so do not declare it — the app's own page tells your coworkers
how they will actually sign in, and declaring a method that is not there would
make that page wrong.
Removing access
Remove an invited person with:
spry uninvite expense-notes --email ryan@yourcompany.com
Uninviting also cancels an invitation they have not used yet.
Access is checked against the current app record on every request. After
removal, the person's next navigation, refresh, form submission, or API request
is refused, even if their 30-day session cookie has not expired. A page already
loaded in their browser cannot be pulled off the screen; it stops working when
it next talks to the app. uninvite only changes the invitation list; it does
not remove owner or named-admin access. Change access.admins and publish again
to remove an admin role.
How your app knows who is using it
Two headers, on every request. That is all.
const email = request.headers['x-spryloom-email']; // who they are
const role = request.headers['x-spryloom-role']; // 'admin' or 'user'
Trust them. Spryloom deletes every x-spryloom-* header that arrives with a
request before it decides anything, so the only way one reaches your app is from
Spryloom. A client that sends one has it removed and never knows: the request
carries on without it, and the app is told who the caller really is.
This matters more than it looks. Anything a browser sends — a form field, a query string, a cookie you set — can be changed by whoever is using it. The header cannot. So when your app records who did something, use the header:
// Right: Spryloom said who this is.
await save({ author: request.headers['x-spryloom-email'], body });
// Wrong: the form said who this is, and a form can say anything.
await save({ author: form.get('author'), body });
A third header, x-spryloom-identity, carries the same facts signed and bound to
the current request method and path, for an app that would rather verify them
itself. Most apps should use the plain headers supplied by Spryloom.
The two roles
admin is you — whoever published the app — and anybody listed in
access.admins. Everybody else is user.
access:
visibility: company
domain: yourcompany.com
admins: [priya@yourcompany.com]
Spryloom does not decide what the difference means. Use it where it matters in your app — who can delete things, who sees everything — and ignore it where it does not.
Roles come from the manifest. To make somebody an admin, add them and publish again.
What your coworkers see when they arrive
Every app serves /~label — a page Spryloom writes, not your app. It says what
the app stores, what it can reach, who can use it, who made it, and when it last
changed.
It is generated from what was actually built, not from what the manifest asked for. An app that declared a database and did not get one says it stores nothing.
It is there because somebody is about to open a thing a colleague generated with
an AI, and "Priya says it's fine" is not enough to go on. Open it from the app's
address by adding /~label. The invitation button signs the person in and
opens the app; it does not automatically show the label page first.
It is behind the same sign-in as the app, and deliberately. It names the administrators by email address, says how many people use the app, and lists what it stores. That is exactly what somebody deciding whether to trust the app needs, and exactly what a stranger who guessed the address should not be handed. So a coworker signs in first and reads it second.
What Spryloom records
Every sign-in, every refused sign-in, every invitation, every publish, and every rollback, with who did it and when.
The dashboard shows the useful operating subset: publishes, rollbacks, archives, successful sign-ins, invitations, and access refusals. The underlying audit log also records sign-in requests, invitation removals, role changes, lifecycle actions, and secret changes. Refusals distinguish between sign-in and the access check after sign-in.
One refusal is usually somebody mistyping their address. Several from outside your company, in a row, is the thing worth looking at, and it is why they are on the page rather than only in the log.
Nothing in that log is ever a token, a session, or a secret — the log refuses to store one, rather than storing it and hoping.