Project Documentation
Centralized auth service for all rokojori projects, deployed at
account.rokojori.com. Developer reference for human and agent contributors.
rokojori-auth is a standalone Express service that owns all identity for the rokojori ecosystem — registration, login, JWT issuance, password reset, roles, products, and global user settings. Every other rokojori project (Roject, future apps, desktop tools) plugs in by verifying the shared JWT. No auth code to write, no user table to maintain in the client app.
The service issues a short-lived access token (1 hour) and a long-lived refresh
token (30 days). For browser clients both are set as HttpOnly cookies
on .rokojori.com, making them automatically available to all subdomains.
Non-browser clients (Electron, CLI, Godot) receive the tokens in the response body
and manage them locally.
Email and password registration and login. Passwords are hashed with bcrypt (cost 10).
On login or register, the server issues an access token cookie and a refresh token
cookie, both scoped to .rokojori.com. The refresh token is also returned
in the response body for non-browser clients.
Browser clients whose access token has expired are redirected to
GET /api/auth/refresh-session?redirect=.... The server reads the
refresh token cookie, rotates both tokens, and redirects back. Non-browser clients
call POST /api/auth/refresh with the refresh token in the request body.
Forgot-password sends a time-limited email link (1 hour). The endpoint is
rate-limited per IP with an escalating response delay (5 s → 15 s → 30 s,
hard block after 20 attempts in 20 minutes) to prevent email spam.
The response is always { ok: true } regardless of whether the
email exists, to avoid leaking account information.
Authenticated users can read their profile, update global settings, change their password (requires current password), and permanently delete their account. Deletion removes the user record and all associated tokens.
Three built-in roles: user (default on registration),
admin (enhanced profile, user list), and superadmin
(can manage roles of other users). Roles are included in the JWT payload so
client apps can gate features without a round-trip. App-specific roles (e.g.
editor of a specific Roject project) stay in the respective app, keyed by
userId.
Bootstrap: set INITIAL_SUPERADMIN_EMAIL in .env.
The first registration with that email is automatically promoted to
superadmin, provided no superadmin exists yet.
Each user record has a products array. Products can be assigned
manually via PATCH /api/admin/users/:id/products (superadmin only)
or automatically when a purchase is confirmed (planned: Polar webhook).
Apps read the product list from the JWT at runtime — no round-trip to
rokojori-auth needed.
Other rokojori services can resolve an email address to a user ID without
a user JWT — useful for migrating member records from email-based to
ID-based storage. The caller authenticates with a shared
SERVICE_SECRET environment variable, not a user token.
Returns { id, email } or 404.
In-memory per-IP rate limits on all sensitive endpoints. Windows and thresholds:
Five self-contained pages served at account.rokojori.com,
shared across all rokojori apps. All pages are plain HTML with inline
CSS and JavaScript — no framework, no build step.
login.html — accepts ?redirect= query paramregister.html — accepts ?redirect= query paramprofile.html — change password, delete account; admin/superadmin see user listforgot-password.htmlreset-password.html — reads ?token= from URL; includes hidden email field for browser password-manager integration
Node.js + Express, TypeScript compiled on the fly with ts-node.
No database — all data lives as JSON files in build/data/
(auto-created on first run). All entity IDs are UUIDs via
crypto.randomUUID(). Email via Nodemailer (SMTPEmailSender),
configured entirely through environment variables.
rokojori-auth/
source/
server/
routes/
auth.ts — all auth + profile endpoints
admin.ts — user list + role management (admin/superadmin)
middleware/
requireAuth.ts — JWT verification (cookie or Authorization header)
requireAdmin.ts — role guards (requireAdmin, requireSuperAdmin)
requireAccess.ts — flexible role+product access rules (copy into client services)
email/
EmailSender.ts — interface
SMTPEmailSender.ts
EmailService.ts — static facade
db.ts — users, refreshTokens, resetTokens (JSON file storage)
roles.ts — role → permissions map, helper functions
rateLimiter.ts — per-IP in-memory rate limiters
index.ts — Express entry point
pages/ — HTML pages (copied to build/app/ by build script)
scripts/
copy-pages.js
workspace/ — this documentation
// users.json
{
"id": "uuid",
"email": "user@example.com",
"passwordHash": "...",
"roles": ["user"],
"products": [
{ "id": "roject-pro", "acquiredAt": "2026-07-13", "source": "polar" }
],
"settings": { "theme": "dark", "language": "en" },
"createdAt": "2026-07-13T00:00:00Z"
}
// refreshTokens.json
{ "token": "uuid", "userId": "uuid", "expiresAt": "..." }
// resetTokens.json
{ "token": "uuid", "userId": "uuid", "expiresAt": "..." }
Access tokens are signed HS256 with JWT_SECRET (shared across all
services). They carry everything a client app needs to know — no round-trip to
rokojori-auth required at runtime.
{
"userId": "uuid",
"email": "user@example.com",
"roles": ["user"],
"products": ["roject-pro"],
"settings": { "theme": "dark" }
}
Running on Server A at account.rokojori.com. nginx handles TLS
termination (Let's Encrypt) and reverse-proxies to the Node.js process on port
3001, managed by a systemd service (rokojori-auth.service).
trust proxy is enabled so the rate limiter sees real client IPs.
Each rokojori service defines an access rule list — a small array
that declares who is allowed in. The JWT already carries both roles
and products, so no round-trip to rokojori-auth is needed at request time.
superadmin is implicitly allowed on every service. No service needs
to include it in its rule list.
type AccessRule = {
role: string; // required — user must have this role
product?: string; // optional — user must also have this product
};
Multiple rules are OR-combined: access is granted if
any rule matches. Within a single rule, role and
product are AND-combined.
// Grant access to:
// • any admin (role alone is sufficient)
// • any user who has the "styles" product
// • any user who has the "premium" product
const rules: AccessRule[] = [
{ role: 'admin' },
{ role: 'user', product: 'styles' },
{ role: 'user', product: 'premium' },
];
// source/server/middleware/requireAccess.ts
import { Request, Response, NextFunction } from 'express';
type AccessRule = { role: string; product?: string };
export function requireAccess( rules: AccessRule[] )
{
return ( req: Request, res: Response, next: NextFunction ): void =>
{
const auth = req.auth;
if ( !auth ) { res.status( 401 ).json( { error: 'Not authenticated' } ); return; }
if ( auth.roles.includes( 'superadmin' ) ) { next(); return; }
const allowed = rules.some( rule =>
auth.roles.includes( rule.role ) &&
( !rule.product || auth.products.includes( rule.product ) )
);
if ( allowed ) { next(); return; }
res.status( 403 ).json( { error: 'Forbidden' } );
};
}
Use after requireAuth. For browser pages that should redirect
rather than return JSON, check req.accepts('html') in the 401/403
branches and redirect to
account.rokojori.com/login?redirect=<current-url>
or back to /.
Each service defines its own rules inline where the router is mounted.
Copy requireAccess.ts into the service's middleware folder —
it has no dependencies beyond the shared AuthPayload type
from requireAuth.ts.
Superadmins can set the full product list on any user via the admin route.
The body is an array of product ID strings; the server records
acquiredAt (now) and source: "manual" for each.
The updated list is reflected in the user's next JWT.
When a user purchases a product through Polar (merchant of record — handles VAT/tax automatically), Polar fires a webhook to rokojori-auth. The handler verifies the Polar signature, finds the user by email, and appends a product entry to their record. The updated product list is included in the next JWT issued for that user. No Polar dependency is needed in any other service.
A dedicated review pass covering: helmet.js headers, CSRF considerations for the cookie flow, refresh token rotation audit, token expiry edge cases, and a review of the rate limiter behaviour under proxy chains.
Roject is the first client app of
rokojori-auth. It is a self-hosted, agent-based IDE deployed at
roject.rokojori.com. After the auth integration is complete, Roject
will drop its own user system entirely and become a JWT-validating client: it
redirects to account.rokojori.com/login for login, reads the shared
cookie, and verifies the JWT locally on every request.
For deeper context on the overall ecosystem, coding conventions, and how to work with the codebase, consult the Roject workspace documentation. It contains guides (writing TypeScript code, backend routes, editor panels), repeatable actions (update history, update outline), and reference docs (Editor Singleton, client/server split). Ask for the Roject workspace docs for those details.