Plan — In Progress

rokojori-auth

Extracting the user system from Roject into a standalone centralized auth service at account.rokojori.com, shared across all rokojori projects.

Why

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.

Two Repos, Clear Responsibilities

rokojori-auth — new repo

A standalone Express service, identical stack to Roject (Node.js, ts-node, JSON file storage). Owns everything identity-related:

  • User registration and login
  • JWT issuance (access token + refresh token)
  • Password reset via email
  • User profile management
  • Roles, permissions, products, global settings
  • Login and register HTML pages (shared across all apps)
c:\rokojori\projects\web-projects\rokojori-auth account.rokojori.com

roject — existing repo, becomes a client

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.

  • Remove source/server/routes/auth.ts
  • Remove source/server/middleware/auth.ts
  • Remove user parts of source/server/db.ts
  • Remove bcryptjs, express-session
  • Remove source/pages/login.html, register.html
  • Remove source/server/email/ (moves to rokojori-auth)
  • Add JWT verification middleware (reads cookie, verifies with shared secret)
  • Login / logout nav links point to account.rokojori.com

Two Login Modes

Browser — redirect flow

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.

Non-browser — direct API call

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.

Same endpoint, both modes

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.

Token Strategy

Access token — short-lived JWT

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.

Refresh token — long-lived, server-side

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.

Cookie on .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.

Login Flow

  1. User visits roject.rokojori.com, not logged in.
  2. Clicks Login → browser goes to account.rokojori.com/login?redirect=https://roject.rokojori.com
  3. User submits credentials on account.rokojori.com/login.html.
  4. rokojori-auth verifies password, issues JWT, sets cookie on .rokojori.com.
  5. Redirects to the redirect URL — cookie is already valid there.
  6. Roject reads and verifies the cookie — user is logged in.

Logout works the same way: link to account.rokojori.com/logout?redirect=..., which clears the cookie and redirects back.

User Data Model

users.json — one record per user

{
  "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"
}

Roles and permissions

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.

Products

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.

Settings

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.

refreshTokens.json

{ "token": "uuid", "userId": "uuid", "expiresAt": "2026-08-12T00:00:00Z" }

resetTokens.json

{ "token": "uuid", "userId": "uuid", "expiresAt": "2026-07-12T01:00:00Z" }

API Endpoints

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

File Structure — rokojori-auth

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

Environment Variables

Shared across all services

JWT_SECRET=...          — same value on all services that verify tokens

rokojori-auth only

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

Adding a New App Later

  1. Add JWT_SECRET env var to the new app.
  2. Add JWT verification middleware (copy from Roject).
  3. Point login/logout links at account.rokojori.com.
  4. Done — no auth code to write, no user table to create.