NukeBase

Authentication

NukeBase provides a built-in cookie-based authentication system. When you configure authPath: ["users"] in your domain setup, authentication endpoints are automatically available and cookies are handled seamlessly.

How it works:

  1. Configure authPath: ["users"] in your domain setup
  2. Use the built-in authentication endpoints from your client
  3. Server automatically sets HTTP cookies (uid, token)
  4. WebSocket connections automatically use these cookies
  5. User information populates the admin object for security rules

What's in core, and what's an extension

Configuring authPath turns on the parts of authentication that every sign-in method needs, regardless of how the user proved who they are:

  • Session validationcheckAuth reads the uid/token cookies on every request and WebSocket upgrade, and populates admin.
  • POST /logout — clears the cookies and revokes the current session token.
  • Hourly token cleanup — expired entries are swept from auth.tokens automatically.

The endpoints that create a session are opt-in extensions you mount from app.js. Pick the one that matches how you want people to sign in — or mount more than one; they all mint the same session cookies and share the same user records.

ExtensionAddsNeeds
extensions/auth POST /login, POST /changepassword Nothing — username + password, hashed with argon2id
extensions/easy-login POST /auth/email, GET /auth/callback, POST /auth/me Nothing — passwordless email links delivered by NukeBase's hosted mail service
extensions/magic-link POST /magic-link, POST /magic-signup, GET /magiclink Your own SendGrid key and sender address
Mounting the password extension in app.js
const nukebase = addDomain({ authPath: ["users"] });

// Enables /login and /changepassword.
// (/createuser ships commented out — accounts are created by the email flows.)
// Omit this and your app is passwordless — /logout and session
// validation keep working either way.
require("./extensions/auth")({
  app: nukebase.app,
  authPath: nukebase.authPath,
  get, set, update, remove, query, generateRequestId, hashToken,
  sessionTokenTtlMs: 24 * 60 * 60 * 1000   // optional, default 24h
});

Not mounted means not routed. If extensions/auth isn't mounted, a POST /login falls through to the static handler and returns 404 — the client SDK's login() will fail on JSON parsing rather than telling you the endpoint is missing. Same for createUser(), changePassword() and magicLink().

Authentication Endpoints

The endpoints below are provided by the extensions in the table above.

Available Endpoints:

  • POST /login - Login with username/password, or resume session via cookies (can also upgrade a demo account that has not yet set credentials by providing username/password). Requires the auth extension.
  • POST /createusercurrently disabled. The route is commented out in extensions/auth.js: account creation is email-only. Use POST /auth/email (easy-login) or POST /magic-signup (magic-link) instead, both of which create the account on first sign-in. The SDK's createUser() will get a 404 until you uncomment it.
  • POST /magic-signup - Create a passwordless account (username/email only). The server creates the account and emails a magic-link sign-in. The user is NOT logged in by this call. Requires the magic-link extension.
  • POST /logout - Clear authentication cookies and revoke the current session token. Built into the engine — always available once authPath is set, no extension required.
  • POST /changepassword - Change user password (requires an active password-backed session). Requires the auth extension.
  • POST /auth/email - Email a sign-in link using NukeBase's hosted mail service — no email provider of your own. Requires the easy-login extension.
  • GET /auth/callback?code=... - Landing URL for an easy-login link. Verifies the code, finds or creates the account, sets session cookies, and 302-redirects.
  • POST /auth/me - Returns { email } for the current session, or { email: null }. Requires the easy-login extension.
  • POST /magic-link - Email a one-time sign-in link to an existing account (15-minute expiry). See Magic Link Authentication.
  • GET /magiclink?token=... - Public landing URL the magic-link email points to. Validates the token, sets session cookies, and 302-redirects to redirectPath (configured when mounting the extension).

Cookies set on success: all auth endpoints that establish a session set two cookies — uid and token — with attributes HttpOnly; Secure; SameSite=Strict; Max-Age=86400; Path=/. That gives you a 24-hour session backed by a server-side hashed token. /logout sets matching Max-Age=0 cookies to clear them.

Because Secure is required, the cookies will not be set over plain http://. Use https:// in production and http://localhost in development (browsers exempt localhost from the Secure requirement).

Built-in security — you don't need to add these yourself:

  • Rate limiting: 5 attempts per 60-second window per client IP on /login, /magic-link and /auth/email. A successful login clears the limiter for that IP.
  • Argon2 password hashing: passwords are stored as argon2id hashes. Plaintext passwords from legacy data are auto-migrated on first successful login.
  • SHA-256 hashed session tokens: the cookie holds the raw token, but only its SHA-256 hash is stored under auth.tokens. A leak of the database does not leak usable session tokens.
  • Hourly token cleanup: a background job sweeps expired entries from auth.tokens and from the rate-limit map. No manual housekeeping is required.
  • Username enumeration resistance: /magic-link returns the same response whether or not the address corresponds to an existing account.

Username and password format constraints:

  • Username (/login): matches /^[a-z0-9_.]{1,32}$/ — lowercase letters, digits, underscore and dot, up to 32 characters. No @, so a password-login username is a handle, not an email address.
  • Password: 8–128 characters, enforced on /changepassword.
  • Email (the passwordless flows): a normal address pattern — +, uppercase and long domains are all accepted. Addresses are lowercased before lookup, and stored under auth.email rather than auth.username.

The two identity fields are separate. Password login looks a user up by auth.username; both email flows look them up by auth.email. A user created by an email flow has no username and cannot sign in through /login, and vice versa — unless you populate both fields yourself.

Behind a reverse proxy? Nothing to configure. The engine decides whether to believe x-real-ip / x-forwarded-for from where the connection actually came from: a Unix socket or a private/loopback peer means a proxy you put there and the header is honored; a public peer address means the open internet and the header is ignored in favour of the peer. A client on a public port therefore cannot rotate headers to reset its own login budget. See Environment Variables for the TRUST_PROXY override, which exists only for unusual topologies.

Login

Use the /login endpoint to login with a username and password. If valid auth cookies are already on the request, the session is resumed automatically — no DB lookup against the password. The endpoint can also upgrade a demo account: if the cookie-session belongs to an account that has no username and no password yet (i.e. a true demo account that has never been upgraded), passing (username, password) attaches credentials to that same UID. Once an account has a username or password, this upgrade path is no longer available — subsequent calls just resume the existing session.

Login
// Import and destructure the methods you need
import createClient from './sdkmod.js';
const { login } = await createClient();

// Login with username and password
const result = await login("username", "password");
if (result && result.status === "Success") {
    console.log('Authenticated as:', result.username);
}

// Resume session (if cookies are already set)
const result2 = await login();
if (result2 && result2.status === "Success") {
    console.log('Session resumed:', result2.uid);
}

// Upgrade a demo account to a full account
// Preconditions: valid demo cookies AND the account currently has
// no username and no password set. After the first upgrade, calling
// login(...) again just resumes the existing session — it will not
// overwrite the username/password.
const result3 = await login("newUsername", "newPassword");
if (result3 && result3.status === "Success") {
    console.log('Demo account upgraded:', result3.username);
}

Create Account

/createuser is commented out in extensions/auth.js as shipped, so everything in this section is inert until you uncomment it. Accounts are created by the email flows instead — /auth/email or /magic-signup — both of which create the user on first sign-in with no separate signup step.

If you re-enable it, /createuser supports:

  • No arguments — creates a demo (anonymous) account and immediately logs the user in.
  • Username + password — creates a full account and immediately logs the user in.

Both modes fail with "Already signed in, logout first" if the caller already has a valid session cookie. For passwordless (email-only) signup, use POST /magic-signup instead — see Magic Link Authentication.

Create account
import createClient from './sdkmod.js';
const { createUser } = await createClient();

// Create a demo/anonymous account (no credentials) — logs in immediately
const result = await createUser();
if (result && result.status === "Success") {
    console.log('Demo account created:', result.uid);
}

// Create a full account with username and password — logs in immediately
const result2 = await createUser("myUsername", "myPassword");
if (result2 && result2.status === "Success") {
    console.log('Account created:', result2.username);
}
// Note: Fails if already signed in - logout first
// For passwordless signup, use POST /magic-signup instead

Logout

Clear authentication cookies to log out the user:

Logout function
import createClient from './sdkmod.js';
const { logout } = await createClient();

const result = await logout();
if (result && result.status === "Success") {
    console.log('Logged out successfully');
}

Change Password

Allow authenticated users to change their password:

Change password function
import createClient from './sdkmod.js';
const { changePassword } = await createClient();

const result = await changePassword("newPassword123");
if (result && result.status === "Success") {
    console.log('Password changed successfully');
} else {
    console.log('Failed to change password');
}

Easy Login — passwordless email, no mail provider

The easy-login extension gives you passwordless email sign-in without setting up SendGrid, Mailgun or any provider of your own. NukeBase delivers the sign-in emails on your behalf (billed to your project) and tells your server which address clicked the link. Everything else stays yours: the accounts live under your authPath, and the sessions are your app's sessions.

Mounting easy-login
const nukebase = addDomain({ authPath: ["users"] });

require("./extensions/easy-login")({
  app: nukebase.app,
  authPath: nukebase.authPath,
  get, set, query, generateRequestId, hashToken,
  redirectPath: "/dashboard",          // optional, default "/"
  brandName: "Acme",                   // optional, shown in the email
  sessionTokenTtlMs: 24 * 60 * 60 * 1000  // optional, default 24h
});
Signing in from the browser
// 1. Ask for a link
const r = await fetch("/auth/email", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "matt@example.com" })
});
const { status, message } = await r.json();
// Always the same generic response, whether or not the account exists

// 2. The user clicks the link in their inbox. It lands on
//    /auth/callback?code=..., which creates the account if it's new,
//    sets the session cookies, and redirects to redirectPath.

// 3. Anywhere after that:
const me = await (await fetch("/auth/me", { method: "POST" })).json();
// → { email: "matt@example.com" }  or  { email: null }

// 4. Sign out with the core endpoint
await fetch("/logout", { method: "POST" });

Credentials come from sys/config.json (API, uid, project, mailKey), written when the project was created. The mailKey is a scoped NukeBase token that can only send sign-in emails for this project — it is not a mail-provider key, and it can be rotated from the dashboard. If any of the four is missing, the extension still mounts but /auth/email returns { status: "Failed", message: "Login not configured" } and logs a warning at startup.

Custom domains just work. The redirect target is built from the Host the request actually arrived on, so a user signing in from your custom domain lands back on it rather than the *.nukebase.com subdomain. NukeBase re-validates that host against the project's known domains, so a spoofed Host header cannot be used as a redirect target.

New addresses create accounts. /auth/callback finds the user by email or creates one — there is no separate signup step. Rate limited to 5 emails per 60-second window per IP, and the response to /auth/email is identical whether or not the address exists, so it cannot be used to enumerate users.

Use this instead of easy-login when you want to send the emails yourself — your own SendGrid account, your own sender address, your own template.

NukeBase has built-in passwordless sign-in via emailed one-time links. Two endpoints power the flow, and account creation can also bootstrap into it.

Setup requirement: Magic-link authentication is an optional extension that requires SendGrid. Mount it in your app.js after addDomain():

const nukebase = addDomain({ authPath: ["users"] });

// Mount magic-link extension
require("./extensions/magic-link")({
  app: nukebase.app,
  authPath: nukebase.authPath,
  query, set, generateRequestId, hashToken,
  sendGridKey: "SG.your-sendgrid-api-key",
  fromEmail: "noreply@yourdomain.com",
  domain: "https://your-app.nukebase.com",
  redirectPath: "/dashboard",       // optional, default "/"
  errorRedirect: "/",               // optional, default "/"
  ttlMs: 15 * 60 * 1000,           // optional, token expiry (default 15 min)
  sessionTokenTtlMs: 24*60*60*1000, // optional, session length (default 24h)
  cookieMaxAgeSeconds: 86400        // optional, cookie Max-Age (default 86400)
});

Without this extension mounted, the /magic-link, /magic-signup, and /magiclink endpoints will not exist. The extension requires sendGridKey, fromEmail, and domain — it throws on startup if any are missing.

POST /magic-link — request a sign-in link for an existing account

Request a magic link
const r = await fetch("/magic-link", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "matt@example.com" })
});
const { status, message } = await r.json();
// status === "Success" whether or not the address exists (anti-enumeration)

Body: { email: string }. The address must match the standard username regex (lowercased internally). The response is the same regardless of whether the account exists, so a bad actor cannot use this endpoint to enumerate users. The IP-based rate limiter applies (5 / 60s).

GET /magiclink?token=... — consume a sign-in link

This is the URL that the email points at; users don't call it from code. On a valid, unexpired token the server:

  1. Generates a new 32-byte session token, stores its SHA-256 hash under users.<uid>.auth.tokens with a 24-hour expiry,
  2. Sets uid and token cookies (HttpOnly; Secure; SameSite=Strict),
  3. Issues a 302 Found redirect to ${domain}${redirectPath} (both configured when mounting the extension).

Tokens are single-use (deleted on consumption) and expire 15 minutes after issue. An invalid or expired token redirects to ?error=invalid_or_expired; a missing token redirects to ?error=missing_token.

Passwordless account creation via /magic-signup

Use the /magic-signup endpoint to create an account that exists only for magic-link sign-in. The server creates the account and immediately sends a sign-in link — the response does not log the user in; they have to click the link in their email. This endpoint is provided by the magic-link extension.

Passwordless signup
// Email-only signup via /magic-signup — user must click the link in their inbox
const r = await fetch("/magic-signup", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ username: "matt@example.com" })
});
const result = await r.json();
// result.status === "Success", result.message tells the user to check email
// (no uid / token returned here — the session is established by /magiclink)

Using Authentication in Security Rules

Once authenticated, the admin object is available in your security rules:

Security rules with authentication
// In your rules.js
module.exports = {
  "users": {
    "$userId": {
      // Only the user themselves can edit
      "write": "admin.uid == $userId",

      // Don't grant read at $userId — it would cascade and expose "private".
      // Split the data into public/private subnodes and grant read on each.
      "public":  { "read": "true" },                    // Anyone can read public profile
      "private": { "read": "admin.uid == $userId" }     // Only the user
    }
  },

  "adminPanel": {
    // Only users with admin role can access
    "read":  "admin.claims.role == 'admin'",
    "write": "admin.claims.role == 'admin'"
  }
};

Security Notes:

  • Use HTTPS in production — the auth cookies are Secure-flagged and will not be set over plain HTTP (browsers exempt localhost for development).
  • Rate limiting on /login, /magic-link and /auth/email is built in (5 attempts per 60-second window per client IP). The client IP is derived from the connection's peer address, so it is already correct behind a reverse proxy and cannot be spoofed by a directly-exposed client — no configuration needed.
  • Expired session tokens are swept automatically every hour — no cleanup script needed.
  • Passwords are hashed with argon2id; session tokens are SHA-256 hashed before storage. Legacy plaintext passwords auto-upgrade on first successful login.
  • generateRequestId(bytes) uses crypto.randomBytes and returns a hex string. Defaults to 8 bytes (16 hex chars); session tokens use 32 bytes (64 hex chars).

Custom Claims

Custom claims let you attach arbitrary data (roles, permissions, plan tiers, etc.) to a user's auth record. Claims are available in security rules and callable functions via admin.claims.

Managing claims from app.js
// Set all claims at once
set(["users", uid, "auth", "claims"], { role: "admin", plan: "pro" });

// Update or add a single claim
update(["users", uid, "auth", "claims"], { role: "editor" });

// Remove a single claim
remove(["users", uid, "auth", "claims", "role"]);
Using claims in security rules
module.exports = {
  "adminPanel": {
    "read": "admin.claims.role == 'admin'",
    "write": "admin.claims.role == 'admin'"
  },
  "premiumContent": {
    "read": "admin.claims.plan == 'pro'"
  }
};

Important: Claims are read when a WebSocket connection is established. If you change a user's claims while they are connected, the changes won't take effect until their next connection (page reload, reconnect, or new login). Users without any claims will have admin.claims default to an empty object {}.

Database Structure for Authentication

The authentication system expects user data to be structured like this:

User data structure

  "users": {
    "ML96SDE5": { // Unique user UID (hex, generated by generateRequestId)
      "auth": {
        "username": "matt123",                    // Lowercased username/email (optional for demo accounts)
        "password": "$argon2id$v=19$m=65536,...", // argon2id hash — never plaintext
        "tokens": {
          // Keys are SHA-256 hashes of the raw session token (the cookie value).
          // Values are expiration timestamps in ms since epoch.
          "8f3a...e21c": 1748357368415,
          "b12d...07ff": 1748357670935
        },
        "claims": { // Optional custom claims (free-form object)
          "role": "admin",
          "plan": "pro"
        }
      }
    }
  }

What's actually stored vs. what the cookie holds:

  • Password: stored as an argon2id hash. A legacy plaintext value will be auto-upgraded to a hash on the next successful login.
  • Session tokens: the cookie holds the raw 32-byte hex token; the database stores only its SHA-256 hash as the key under auth.tokens. You cannot reconstruct a valid cookie from the database alone.
  • Don't try to read tokens out of auth.tokens at runtime — they're hashes, not the values you'd put back into a cookie.

Token cleanup is automatic. A background sweep runs hourly and removes any auth.tokens entries whose expiry has passed, plus stale entries from the in-memory rate-limit map. You don't need to schedule your own cleanup.