Architecture
Authentication & Authorization
Multi-layered authentication system in StellarStack
Authentication & Authorization
StellarStack uses a multi-layered authentication system to secure all components.
Authentication Layers
- User Authentication - Better Auth (users accessing the platform)
- Daemon Authentication - mTLS + API tokens (nodes connecting to control plane)
- Console Authentication - Signed JWTs (users accessing server consoles)
User Authentication (Better Auth)
Configuration
// apps/api/src/auth.ts
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { prisma } from "./db";
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "postgresql",
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
},
session: {
strategy: "jwt", // or "database" for more control
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // Update session every 24 hours
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
discord: {
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
},
},
});Session Flow
- User signs in via Better Auth
- Better Auth creates session (stored in Redis/DB)
- Session token returned as HTTP-only cookie
- All API requests include cookie automatically
- Hono middleware validates session on each request
API Middleware
// apps/api/src/middleware/auth.ts
import { auth } from "../auth";
export const authMiddleware = async (c, next) => {
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
if (!session) {
return c.json({ error: "Unauthorized" }, 401);
}
c.set("user", session.user);
c.set("session", session.session);
await next();
};Role-Based Access Control (RBAC)
Role Hierarchy
Super Admin
│
├── Admin
│ │
│ ├── Moderator
│ │
│ └── Support
│
└── User (default)
│
└── Subuser (per-server)Permission Matrix
export const PERMISSIONS = {
// Server permissions
"server.view": "View server details",
"server.console": "Access server console",
"server.console.send": "Send console commands",
"server.files.read": "Read server files",
"server.files.write": "Write server files",
"server.files.delete": "Delete server files",
"server.start": "Start server",
"server.stop": "Stop server",
"server.restart": "Restart server",
"server.kill": "Force kill server",
"server.delete": "Delete server",
"server.settings": "Modify server settings",
"server.subusers": "Manage subusers",
"server.schedules": "Manage schedules",
"server.backups": "Manage backups",
"server.databases": "Manage databases",
// Node permissions (admin only)
"node.view": "View node details",
"node.create": "Create nodes",
"node.delete": "Delete nodes",
"node.settings": "Modify node settings",
// User permissions (admin only)
"user.view": "View user details",
"user.create": "Create users",
"user.delete": "Delete users",
"user.suspend": "Suspend users",
"user.servers": "Manage user servers",
// Platform permissions (super admin only)
"platform.settings": "Modify platform settings",
"platform.blueprints": "Manage blueprints",
"platform.billing": "Access billing",
} as const;Permission Check
// middleware/permission.ts
export const requirePermission = (permission: string) => {
return async (c, next) => {
const user = c.get("user");
const serverId = c.req.param("serverId");
const hasPermission = await checkPermission(user.id, serverId, permission);
if (!hasPermission) {
return c.json({ error: "Forbidden" }, 403);
}
await next();
};
};
// Usage
app.post("/servers/:serverId/start",
authMiddleware,
requirePermission("server.start"),
startServerHandler
);Console Token Flow
Step 1: User requests console access
────────────────────────────────────
User ──▶ GET /api/v1/servers/{serverId}/console-token
Step 2: API validates permissions
─────────────────────────────────
API checks:
├── Is user authenticated?
├── Does user own this server OR have subuser access?
├── Does user have "server.console" permission?
└── Is server on an online node?
Step 3: API generates signed console token
──────────────────────────────────────────
API ──▶ {
"consoleToken": "strk_console_xxxxx",
"nodeHost": "node-us-west.stellarstack.app",
"nodePort": 5000,
"expiresAt": "2024-12-09T12:05:00Z",
"permissions": {
"canRead": true,
"canWrite": true
}
}
Step 4: Frontend connects directly to daemon
────────────────────────────────────────────
Frontend ──▶ WebSocket: wss://node.example.com:5000/console
Headers: { "Authorization": "Bearer strk_console_xxxxx" }
Step 5: Daemon validates token
──────────────────────────────
Daemon:
├── Verifies JWT signature
├── Checks token expiration
├── Checks serverId matches container
└── Extracts permissionsConsole Token Structure
interface ConsoleTokenPayload {
// Standard JWT claims
iss: "stellarstack";
sub: string; // User ID
aud: string; // Node ID
exp: number; // Expiration
iat: number; // Issued at
jti: string; // Unique token ID
// Custom claims
serverId: string;
containerId: string;
permissions: {
canRead: boolean;
canWrite: boolean;
};
metadata: {
username: string;
ip: string;
};
}Daemon Authentication
Registration Flow
- Admin generates registration token in panel
- Daemon uses token to register with control plane
- Daemon receives long-lived API token and certificate
- Daemon connects to Redis with token authentication
Token Types
| Token Type | Format | Purpose | Lifetime |
|---|---|---|---|
| Registration | strk_reg_xxxxx | One-time node registration | 24 hours |
| Node API | strk_node_xxxxx | Daemon ↔ Redis communication | 1 year (rotatable) |
| Console | strk_console_xxxxx | User ↔ Daemon WebSocket | 5 minutes |