Ship secure user access fast: let Docly handle login, you handle the permissions

Clarification
Skip the authentication tax — let Docly's built-in login handle passwords, resets, sessions, 2FA and threat protection for you (enterprise-grade security in minutes, no auth code to write or maintain): invite users with docly.addFolderShare("/", email, "V", inviteMessage) and let Docly's login portal handle authentication, passwords, resets and sessions, while application rights (roles, per-customer access) live in a local user-registry folder with one document per email address, looked up via request.user in every API script.
Applies to: Folder sharesLogin portalAPI scripts

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.user in 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, so docly.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 (#/ConfigInviteMessage) 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/Admin flags or delete their own user document.
  • removeWebAccess must 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.