Plan — In Progress
Extracting the user system from Roject into a standalone centralized auth service
at account.rokojori.com, shared across all rokojori projects.
Roject currently has a self-contained user system (registration, login, sessions
via express-session, bcrypt password hashing, JSON file storage).
Any future rokojori project — a personal website, desktop tools, other services —
would need to duplicate this, resulting in multiple disconnected user systems with
no shared identity.
The goal is one user record per person across all rokojori projects. Roles, permissions, purchased products, and global settings all live in one place. New projects plug in by verifying a JWT — no auth code to write, no user table to maintain.
Payments will be handled by Polar (merchant of record — handles
VAT/tax automatically). Polar is not used as a user store; it only tells us who
paid for what. That purchase record is then written to the user in
rokojori-auth via a webhook and stored there permanently.
A standalone Express service, identical stack to Roject (Node.js, ts-node, JSON file storage). Owns everything identity-related:
Roject drops all user management and becomes a JWT-validating client.
Its own data (groups, projects, files, layouts) continues to be stored
locally — it just references userId from the JWT instead
of a local user table.
source/server/routes/auth.tssource/server/middleware/auth.tssource/server/db.tsbcryptjs, express-sessionsource/pages/login.html, register.htmlsource/server/email/ (moves to rokojori-auth)account.rokojori.com
Used by web apps (Roject, website). App links to
account.rokojori.com/login?redirect=..., user logs in on the
HTML page, cookie is set on .rokojori.com, browser is redirected
back. No token handling needed in the app.
Used by Electron, mobile apps, Godot, CLI tools, or any client that manages its own storage. POST credentials directly to the login endpoint and receive tokens in the response body — no browser, no redirect, no cookie.
POST https://account.rokojori.com/api/auth/login
{ "email": "...", "password": "..." }
→ { "accessToken": "...", "refreshToken": "..." }
The client stores the tokens locally (file, memory, secure storage) and sends the access token on every request as a header:
Authorization: Bearer <accessToken>
When the access token expires, the client calls
POST /api/auth/refresh with the refresh token to get a new one —
no re-login needed.
POST /api/auth/login always returns JSON with the tokens.
The HTML login page additionally sets the cookie and performs the redirect
client-side after reading the response. Non-browser clients ignore the cookie
and just use the response body. No separate endpoints needed.
Signed with a shared JWT_SECRET (HS256). Contains everything
an app needs to know about the user — no database call required at runtime:
{
"userId": "uuid",
"email": "user@example.com",
"roles": ["user"],
"products": ["roject-pro"],
"settings": { "theme": "dark", "language": "en" }
}
Lifetime: ~1 hour. Apps verify the signature locally — no round-trip to
rokojori-auth on every request.
A random UUID stored in refreshTokens.json on
rokojori-auth. Used only to request a new access token when
the current one expires. Lifetime: 30 days. Invalidated on logout.
.rokojori.com
On login, rokojori-auth sets the access token as a cookie
on the .rokojori.com domain. This makes it automatically
available to all subdomains (roject.rokojori.com,
account.rokojori.com, future services) without any extra
token-passing logic.
Web apps read the cookie directly. Desktop/CLI apps (e.g. Electron later)
store the token locally and send it in the Authorization header.
roject.rokojori.com, not logged in.account.rokojori.com/login?redirect=https://roject.rokojori.com
account.rokojori.com/login.html.rokojori-auth verifies password, issues JWT, sets cookie on
.rokojori.com.redirect URL — cookie is already valid there.
Logout works the same way: link to
account.rokojori.com/logout?redirect=..., which clears the cookie
and redirects back.
{
"id": "uuid",
"email": "user@example.com",
"passwordHash": "...",
"roles": ["user"],
"products": [
{ "id": "roject-pro", "acquiredAt": "2026-07-12", "source": "polar" }
],
"settings": {
"theme": "dark",
"language": "en"
},
"createdAt": "2026-07-12T00:00:00Z"
}
Role types are defined in code (a TypeScript object or JSON file) mapping role names to permission sets. The user record stores only the role name — the permissions are derived at runtime.
{
"admin": ["manage-users", "manage-products", "access-all"],
"user": ["access-own"]
}
App-specific roles (e.g. "editor of project X" in Roject) are never stored
in rokojori-auth — they stay in the respective app, keyed by
userId.
When Polar fires a purchase webhook, a record is appended to the user's
products array. Apps check product IDs from the JWT at runtime —
no Polar dependency needed outside of the webhook handler.
A free JSON object for global preferences (theme, language, timezone, etc.) that should be consistent across all apps. App-specific preferences (Roject panel layout, editor config) stay in the respective app.
{ "token": "uuid", "userId": "uuid", "expiresAt": "2026-08-12T00:00:00Z" }
{ "token": "uuid", "userId": "uuid", "expiresAt": "2026-07-12T01:00:00Z" }
POST /api/auth/register — create user, issue tokens
POST /api/auth/login — verify password, issue tokens
POST /api/auth/logout — invalidate refresh token, clear cookie
POST /api/auth/refresh — swap refresh token for new access token
POST /api/auth/forgot-password — send password reset email
POST /api/auth/reset-password — consume reset token, set new password
GET /api/auth/me — return user data from access token
PATCH /api/auth/me/settings — merge-update global settings
rokojori-auth/
source/
server/
routes/
auth.ts — all auth endpoints
middleware/
requireAuth.ts — JWT verification middleware
email/
EmailSender.ts — interface
SMTPEmailSender.ts — Nodemailer implementation
EmailService.ts — static facade
roles.ts — role → permissions map
db.ts — user, refresh token, reset token storage
index.ts — Express app entry point
pages/
login.html — accepts ?redirect= param
register.html — accepts ?redirect= param
profile.html — view/edit email, change password
scripts/
copy-pages.js
package.json
tsconfig.json
tsconfig.client.json
tsconfig.ts-node.json
.gitignore
.env — never committed
JWT_SECRET=... — same value on all services that verify tokens
SMTP_HOST=smtp.ionos.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=noreply@rokojori.com
SMTP_PASS=...
SMTP_FROM=noreply@rokojori.com
PORT=3001 — or whichever port nginx proxies to
JWT_SECRET env var to the new app.account.rokojori.com.