Critical security configurations, vulnerability fixes, and hardening guidelines for production RestAI deployments
This guide covers security best practices for deploying RestAI in production, including critical fixes from the Phase A security audit and ongoing hardening recommendations.
RestAI underwent a comprehensive security audit on 2026-02-09 that identified 124 security issues (18 critical, 29 high, 44 medium, 33 low). Phase A addressed the 9 most critical blockers that would have prevented safe production deployment. Review PHASE-A-CHANGELOG.md for complete details.
If deployed without environment variables, the API would silently use public default secrets. Attackers could forge tokens and impersonate any user.Current fix:
if (!process.env.JWT_SECRET || !process.env.JWT_REFRESH_SECRET) { throw new Error("JWT_SECRET and JWT_REFRESH_SECRET environment variables are required");}const JWT_SECRET: string = process.env.JWT_SECRET;const JWT_REFRESH_SECRET: string = process.env.JWT_REFRESH_SECRET;
The server now refuses to start without proper secrets configured.Action required:
1
Generate strong secrets
# Generate two different secretsopenssl rand -base64 48openssl rand -base64 48
Severity: CRITICAL File: apps/api/src/lib/jwt.ts:49The vulnerability:Access tokens previously had an 8-hour expiration. A stolen token (via XSS, network interception, or physical access) remained valid for 8 hours even after the user logged out.Current fix:Access tokens now expire in 15 minutes:
The frontend automatically refreshes tokens using the refresh token (7-day expiry).Impact: Reduces attack window from 8 hours to 15 minutes for compromised tokens.
Action required:Set CORS_ORIGINS in production .env:
# Single domainCORS_ORIGINS=https://app.yourrestaurant.com# Multiple domains (no spaces after commas)CORS_ORIGINS=https://app.yourrestaurant.com,https://staging.yourrestaurant.com
Never use CORS_ORIGINS=* in production. This allows any malicious website to make authenticated requests on behalf of your users.
Severity: CRITICAL File: apps/api/src/routes/customer.ts (GET /:branchSlug/:tableCode/check-session)The vulnerability:The public endpoint check-session returned the full customer JWT token:
Severity: CRITICAL (UX/Reliability) Files: apps/web/src/app/global-error.tsx, (dashboard)/error.tsx, (customer)/error.tsxThe vulnerability:Any unhandled JavaScript error caused a blank white screen with no recovery mechanism. Users had to manually reload the page.Current fix:Three levels of error boundaries:
Severity: CRITICAL (UX) File: apps/web/src/stores/customer-store.tsThe vulnerability:Customer store only persisted token and sessionId to sessionStorage. On page reload:
branchSlug and tableCode became null
Navigation links broke (/${null}/${null}/profile)
Restaurant name disappeared from header
Current fix:All 7 fields are now persisted and restored:
Severity: LOW File: .env.exampleAll environment variables are now documented with secure defaults and clear comments. See Environment Variables for complete reference.
Drizzle ORM uses parameterized queries by default:
// SAFE - Drizzle parameterizes automaticallyconst user = await db.select() .from(schema.users) .where(eq(schema.users.email, userEmail));// UNSAFE - Never use raw string concatenation// const user = await db.execute(`SELECT * FROM users WHERE email = '${userEmail}'`);
# Generate new secretsNEW_JWT_SECRET=$(openssl rand -base64 48)NEW_JWT_REFRESH_SECRET=$(openssl rand -base64 48)# Update .env and restartdocker-compose down# Update .env with new secretsdocker-compose up -d