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:
- Configure
authPath: ["users"]in your domain setup - Use the built-in authentication endpoints from your client
- Server automatically sets HTTP cookies (uid, token)
- WebSocket connections automatically use these cookies
- User information populates the
adminobject for security rules
Authentication Endpoints
NukeBase automatically provides these authentication endpoints when authPath is configured:
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)
- POST /createuser - Create a new account. Two modes:
- No credentials → demo account (anonymous, immediate login)
- Username + password → full account, immediate login
POST /magic-signupinstead. - 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
- POST /changepassword - Change user password (requires an active password-backed session)
- 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,/createuser, and/magic-link. 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.tokensand from the rate-limit map. No manual housekeeping is required. - Username enumeration resistance:
/magic-linkreturns the same response whether or not the address corresponds to an existing account.
Username and password format constraints:
- Username: matches
/^[a-z0-9@.]{1,32}$/. Inputs are lowercased and trimmed before validation, so case-variant duplicates can't coexist. - Password: 8–128 characters. Enforced on both
/createuserand/changepassword.
The same regex powers the email-based magic-link flow, so any address you accept must also fit it (no +, no uppercase, no longer than 32 chars).
Behind a reverse proxy? TRUST_PROXY is enabled by default, so the rate limiter honors x-real-ip / x-forwarded-for headers. If your server is not behind a reverse proxy, set TRUST_PROXY="false" or TRUST_PROXY="0" so clients cannot rotate those headers to bypass the limiter.
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.
// 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
Create a new account using the /createuser endpoint. Two modes are supported:
- 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.
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:
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:
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');
}
Magic Link Authentication
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
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:
- Generates a new 32-byte session token, stores its SHA-256 hash under
users.<uid>.auth.tokenswith a 24-hour expiry, - Sets
uidandtokencookies (HttpOnly; Secure; SameSite=Strict), - Issues a
302 Foundredirect 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.
// 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:
// 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 exemptlocalhostfor development). - Rate limiting on
/login,/createuser, and/magic-linkis built in (5 attempts per 60-second window per client IP). If you sit behind a reverse proxy, setTRUST_PROXY=trueso the limiter sees the real client IP. - 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)usescrypto.randomBytesand 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.
// 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"]);
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:
"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
argon2idhash. 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.tokensat 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.