Ship secure user access fast: let Docly handle login, you handle the permissions
What you'll see
You are building a Docly site where external users (customers, partners) must log in to see their own data, and you are tempted to implement your own user accounts with passwords — meaning you would also own password hashing, "forgot password" e-mails, session handling, brute-force protection and 2FA. None of that is necessary: Docly's login portal already provides all of it. What the platform does not model is your application-level rights — admin vs. regular user, which customers a user can see, whether they can upload. Granting Browse/Modify shares to every user would expose the raw folder structure; managing rights only in your own documents would leave users unable to log in at all.
What's actually happening
The pattern is a two-layer access model (reference implementation: the Dinkalibrering workspace — adminmodul, velkomstmodul and the Dinkalibrering.no data site):
- Layer 1 — authentication (Docly shares). A folder share on the site root carrying only the
"V"(Visit) flag lets the user sign in through Docly's login portal and reach the published site — nothing more.docly.addFolderShare("/", email, "V", message)both creates the share and sends the invitation e-mail with your message text. The platform owns the entire credential lifecycle — password storage, password resets, session cookies, lockout protection — so your code never sees or stores a password. After login,request.userin every API script holds the authenticated e-mail address. - Layer 2 — authorization (local user registry). A folder (e.g.
Brukere) holds one schema-bound document per user, filename = e-mail address, sodocly.getFile("Brukere/" + request.user)is the entire identity join. The document carries the application rights: an active flag (Tilgang), an admin flag (Admin), per-customer permissions (Tilganger: [{Kunde, Opplasting}]), plus audit fields (Invitert,SistInnlogging).
Because the share only grants Visit, users can authenticate but cannot browse or modify anything directly — every read and write goes through server-side API scripts that check the registry document first. The two layers also stay independently inspectable: docly.getFolderShares("/") returns each share's AllowVisit/AllowAdmin flags and its invitation state (Accepted, Since), which you can join against the registry to show per-user status such as "unanswered invitation since <date>" or "invitation declined / expired".
A separate public landing page (welcome module) needs no access control at all — its login button simply links to the protected module's path; Docly's portal takes over from there and sets the session cookie.
What to do
1. Model the user registry. Create a Brukere folder accepting a Bruker schema whose title field is the e-mail address (TitleFieldPlaceholder: "E-post", ForceGUIDFilename: false). Fields: Navn, Tilgang (active = may log in), Admin, hidden Invitert and SistInnlogging, and a Tilganger table of {Kunde, Opplasting} rows databound to the customer schema. Validate on save: filename must pass docly.isEmail(), and non-admins must have at least one customer access row.
2. Grant and revoke web access from the save pipeline. When a user document is saved with the active flag on, add the share; when deactivated or deleted, remove it. Keep both in a shared library:
export function addWebAccess(username) {
let shares = docly.getFolderShares("/");
if (!shares.Shares.some(s => s.User.toLowerCase() == username.toLowerCase() && s.AllowVisit)) {
let config = docly.getFile("#/Config");
// Creates the share AND sends the invitation e-mail
docly.addFolderShare("/", username, "V", config.InviteMessage);
docly.patchFile("/Brukere/" + username, { "Invitert": /* yyyy-MM-ddTHH:mm */ });
return true;
}
return false;
}
export function removeWebAccess(username) {
let shares = docly.getFolderShares("/");
// Only remove pure visit-shares - developer/admin shares survive
if (shares.Shares.some(s => s.User.toLowerCase() == username.toLowerCase()
&& s.AllowVisit && !s.AllowAdmin && !s.AllowBrowse && !s.AllowCreate
&& !s.AllowInvite && !s.AllowModify && !s.AllowPublish)) {
docly.removeFolderShare("/", username);
return true;
}
return false;
}Keep the invitation text in a config document (#/Config → InviteMessage) so admins can edit it without touching code.
3. Gate every API script through an access library. All authorization resolves from request.user against the registry — never trust client input:
export function isUserAdmin() {
let user = docly.getFile("Brukere/" + request.user);
return user != null && user.Admin && user.Tilgang;
}
export function isDoclyAdmin() { // platform developers, distinct from app admins
let shares = docly.getFolderShares("/");
return shares.Shares.some(s => s.User.toLowerCase() == request.user.toLowerCase() && s.AllowAdmin);
}
export function checkFolderAccess(folder, write, customerId) {
if (isUserAdmin()) return true;
if (!customerId || folder.Name != "Sertifikater") return false;
let user = getUser(); // resolves Tilganger from the registry document
return user.Tilganger.find(t => t.Kunde == customerId && (!write || t.Opplasting));
}4. Surface invitation status in the admin UI. Join the registry against the shares so admins see who has accepted:
let shares = docly.getFolderShares("/");
for (let f of users) {
let share = shares.Shares.find(s => s.User.toLowerCase() == f.filename.toLowerCase() && s.AllowVisit);
if (share) {
f.WebTilgangSiden = share.Accepted
? docly.format(new Date(share.Since), "dd.MM.yyyy")
: "Ubesvart invitasjon " + docly.format(new Date(share.Since), "dd.MM.yyyy");
} else if (f.Tilgang) {
f.WebTilgangSiden = "Invitasjon avslått / utløpt"; // active in registry but share gone
}
}5. Track logins. In the API endpoint the app calls on load, patch SistInnlogging on the registry document (once per day) and write an activity-log entry. Sign out with docly.logOut().
6. Safety rules from the reference implementation:
- Users cannot change their own
Tilgang/Adminflags or delete their own user document. removeWebAccessmust check that the share is a pure visit-share before removing it — otherwise deactivating a user who is also a developer would revoke their workspace access.- The public landing page links to the protected module path (e.g.
/admin); unauthenticated visitors hitting it are sent through Docly's login portal automatically — you write no login UI yourself. - Skip activity-logging for
isDoclyAdmin()users so developer traffic doesn't pollute customer audit logs.
Reference implementation: L:\Dinkalibrering (workspace) — see Dinkalibrering (adminmodul)\Libs\AccessLib.js, API\DoclyForm\SaveFile.js, API\GetFolder.js and the Bruker schema on Dinkalibrering.no.