feat: GPU offload, signup limit, load tests & capacity planning

- Route all AI workloads (Ollama, STT, TTS, Image Gen) to GPU server
  (192.168.178.11) via LAN instead of host.docker.internal
- Upgrade default model to gemma3:12b and max concurrent to 5
- Add daily signup limit service (MAX_DAILY_SIGNUPS env var)
- Add GET /api/v1/auth/signup-status public endpoint
- Add k6 load test suite (web-apps, auth, sync-websocket, ollama)
- Add capacity planning documentation
- Fix: add eslint-config to sveltekit-base and calendar Dockerfiles

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-28 21:14:24 +01:00
parent 16367384c7
commit 9276d9a212
12 changed files with 683 additions and 14 deletions

View file

@ -15,6 +15,7 @@ import { jwtAuth } from './middleware/jwt-auth';
import { serviceAuth } from './middleware/service-auth';
import { initializeEmail } from './email/send';
import { SecurityEventsService, AccountLockoutService } from './services/security';
import { SignupLimitService } from './services/signup-limit';
import { ApiKeysService } from './services/api-keys';
import { createAuthRoutes } from './routes/auth';
import { createGuildRoutes } from './routes/guilds';
@ -31,6 +32,7 @@ const auth = createBetterAuth(config.databaseUrl);
initializeEmail(config.smtp);
const security = new SecurityEventsService(db);
const lockout = new AccountLockoutService(db);
const signupLimit = new SignupLimitService(db);
const apiKeysService = new ApiKeysService(db);
// ─── App ────────────────────────────────────────────────────
@ -61,7 +63,7 @@ app.get('/.well-known/openid-configuration', async (c) => auth.handler(c.req.raw
// ─── Custom Auth Endpoints ──────────────────────────────────
app.route('/api/v1/auth', createAuthRoutes(auth, config, security, lockout));
app.route('/api/v1/auth', createAuthRoutes(auth, config, security, lockout, signupLimit));
// ─── Guilds ─────────────────────────────────────────────────

View file

@ -9,6 +9,7 @@ import { Hono } from 'hono';
import type { AuthUser } from '../middleware/jwt-auth';
import type { BetterAuthInstance } from '../auth/better-auth.config';
import type { SecurityEventsService, AccountLockoutService } from '../services/security';
import type { SignupLimitService } from '../services/signup-limit';
import type { Config } from '../config';
import { sourceAppStore, passwordResetRedirectStore } from '../auth/stores';
@ -16,15 +17,37 @@ export function createAuthRoutes(
auth: BetterAuthInstance,
config: Config,
security: SecurityEventsService,
lockout: AccountLockoutService
lockout: AccountLockoutService,
signupLimit: SignupLimitService
) {
const app = new Hono<{ Variables: { user: AuthUser } }>();
// ─── Registration ────────────────────────────────────────
// ─── Signup Status (public) ─────────────────────────────
app.get('/signup-status', async (c) => {
const status = await signupLimit.getStatus();
return c.json(status);
});
app.post('/register', async (c) => {
const body = await c.req.json();
// Check daily signup limit
const limitCheck = await signupLimit.checkLimit();
if (!limitCheck.allowed) {
return c.json(
{
error: 'Registration limit reached',
message: 'Das tägliche Registrierungslimit ist erreicht. Versuche es morgen wieder.',
spotsRemaining: 0,
resetsAt: limitCheck.resetsAt,
},
429
);
}
// Store source app URL for email verification redirect
if (body.sourceAppUrl && body.email) {
sourceAppStore.set(body.email, body.sourceAppUrl);

View file

@ -0,0 +1,93 @@
/**
* Signup Limit Daily registration cap ("Organic Growth Gate")
*
* Limits new registrations per day to protect hardware and
* enable organic growth. Uses PostgreSQL security_events table
* (no Redis dependency needed).
*
* Configure via MAX_DAILY_SIGNUPS env var (default: 0 = unlimited).
*/
import { sql } from 'drizzle-orm';
import type { Database } from '../db/connection';
export class SignupLimitService {
private maxDaily: number;
constructor(private db: Database) {
this.maxDaily = parseInt(process.env.MAX_DAILY_SIGNUPS || '0', 10);
}
/** Check if registration is allowed right now */
async checkLimit(): Promise<{
allowed: boolean;
current: number;
limit: number;
resetsAt: string;
}> {
// 0 = unlimited (feature disabled)
if (this.maxDaily <= 0) {
return { allowed: true, current: 0, limit: 0, resetsAt: '' };
}
const todayCount = await this.getTodayCount();
const midnight = new Date();
midnight.setHours(24, 0, 0, 0);
return {
allowed: todayCount < this.maxDaily,
current: todayCount,
limit: this.maxDaily,
resetsAt: midnight.toISOString(),
};
}
/** Count registrations today (UTC) */
private async getTodayCount(): Promise<number> {
try {
const result = await this.db.execute(
sql`SELECT COUNT(*) as count
FROM auth.security_events
WHERE event_type = 'REGISTER'
AND created_at >= CURRENT_DATE
AND created_at < CURRENT_DATE + INTERVAL '1 day'`
);
const row = (result as any)[0];
return row ? Number(row.count) : 0;
} catch {
// On error, allow registration (fail open)
return 0;
}
}
/** Public status for the signup page */
async getStatus(): Promise<{
registrationOpen: boolean;
spotsRemaining: number | null;
totalToday: number;
limit: number;
resetsAt: string;
}> {
if (this.maxDaily <= 0) {
return {
registrationOpen: true,
spotsRemaining: null,
totalToday: 0,
limit: 0,
resetsAt: '',
};
}
const todayCount = await this.getTodayCount();
const midnight = new Date();
midnight.setHours(24, 0, 0, 0);
return {
registrationOpen: todayCount < this.maxDaily,
spotsRemaining: Math.max(0, this.maxDaily - todayCount),
totalToday: todayCount,
limit: this.maxDaily,
resetsAt: midnight.toISOString(),
};
}
}