Self-serve workspaces¶
Public-facing deployments (a demo of an app anyone can sign into with Google or
GitHub) need users with no workspace to get one without a Duar admin in the
loop. With SELF_SERVE_ENABLED=true, Duar hosts onboarding pages under
/onboard where a user joins a workspace through a one-time invitation
link (the encouraged path) or creates one and becomes its owner.
Everything is off by default. Consortium/enterprise deployments leave the flag
unset; the only behavioral change they see in this release is that
proxy-mode POST /workspaces now returns 403 (it was open to any signed-in
user) — the CSP header and the /admin/system/settings block also change, as
the changelog notes.
How it works¶
- Pages are server-rendered by Duar (no JavaScript; fonts are self-hosted
under
/static/fonts) and use Duar's own IdP client — the same code flow proxy mode and the admin panel use. Sign-in audits and security signals fire exactly as for any login. - Invitations are one-time links minted by a workspace owner/admin on
/onboard/invites. A link is shown once, expires after 7 days, can grantviewer,editor, oradmin(neverowner), can optionally be locked to one email address, and is only valid while the inviter is still an owner/admin of that workspace. Nobody joins a workspace without their own click — the legacy direct-add endpoint is disabled in self-serve mode. - Creation is capped per user (
SELF_SERVE_MAX_WORKSPACES_PER_USER,0= join-only) and instance-wide per hour (SELF_SERVE_MAX_CREATES_PER_HOUR); slugs are generated.
Flows¶
Invitee with a link — opens {DUAR}/onboard?code=… → signs in → "Join
Workspace as role" → Join → Continue to app (when the link carries
a return_to) → signs into the app (silent; the IdP session is live).
New user, no invite — app's "Sign up" link → {DUAR}/onboard?return_to=…
→ signs in → Create a workspace → Continue to app.
Workspace admin — app's "Invite people" link →
{DUAR}/onboard/invites?workspace={id}&return_to=… → role (+ optional email
lock) → Create invite link → copy the link, send it however you like.
Duar does not send email.
Workspace admin — manage members — the same Manage workspaces page lists
the workspace's members with their roles: change a role (only owners can grant
or demote owner; the last owner can never be demoted), remove a member
(their live app sessions are revoked), or rename the workspace. Any member can
leave a workspace from the /onboard/home list — except its last owner.
Removing a member or leaving revokes that user's sessions on every app on this
Duar instance, not just the workspace they left — token revocation is per
user, not per workspace.
App integration¶
Two links; no SDK changes:
<a href="https://duar.example/onboard?return_to=https://app.example/login">Sign up / Join a workspace</a>
<a href="https://duar.example/onboard/invites?workspace=WORKSPACE_ID&return_to=https://app.example/login">Invite people</a>
return_to must be on an origin registered in the app's Service App
allowed_origins (the same list AuthZ CORS uses) and should be a page that
starts sign-in: after onboarding the app has no session yet. In AuthZ mode
call silentLogin('google') with an explicit provider (the SDK forgets the
provider once a zero-workspace callback has been handled) or show a
"Sign in" button.
Show the "Sign up" link permanently on the login page and make your
errorComponent say "No workspace yet — sign up" for the zero-workspace error.
Deployment checklist¶
SELF_SERVE_ENABLED=true; reviewSELF_SERVE_MAX_WORKSPACES_PER_USERandSELF_SERVE_MAX_CREATES_PER_HOUR.- An enabled public organization must exist (it does by default) — otherwise unclaimed email domains cannot sign in at all.
- Configure Duar's own IdP client:
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET, and register{BASE_URL}/onboard/callback/googleas a redirect URI on the same Google OAuth client the app uses. - Prefer Google only: GitHub accounts are cheap for bots. Entra is not
recommended on a public instance; if used it must be single-tenant and the
app registration must emit
xms_edov— onboarding sign-in fails with 403 unless the claim is present. TIERmust include the public listener (publicorall). Path-prefix deployments must route{prefix}/static/*to Duar as well as{prefix}/onboard/*— the pages load their fonts from there.- Raise
RATE_LIMIT_AUTHZ_RESOLVE(one bucket per calling service) and put per-IP rate limiting at the edge; edge logs will contain single-use?code=URLs. COOKIE_SECURE=trueand a realSESSION_SECRET_KEY.
Security model¶
Session cookie is SameSite=Lax, HttpOnly, 10-minute sliding window; every
form carries a CSRF token; pages are Cache-Control: no-store; return_to is
allowlisted; invite codes are 256-bit, stored hashed, claimed by an atomic
single-use update; invitations die with the inviter's standing; org
allowlists are enforced on redemption; every create/accept/revoke is written
to the activity log and the security event stream.