saassy
opinionated SaaS starter

Stop rebuilding auth and admin for every new project.

saassy is a single-tenant SaaS starter that ships auth, user management, an admin panel with impersonation and audit logging, and a reverse proxy — as four reusable Docker containers that you never touch. Replace only the project-specific frontend and ship.

What you get

Getting started

Clone, configure two env vars, and run one command:

git clone https://github.com/sssemil/saassy.git
cd saassy
cp .env.example .env
# edit .env: set POSTGRES_PASSWORD and JWT_SECRET
# (generate JWT_SECRET with: openssl rand -base64 32)
docker compose up -d --build

1. Visit the app

Open http://localhost (or whatever CADDY_HTTP_PORT you set):

2. Sign in

Enter your email on /login. The magic link shows up in user-gateway's logs — or set a real RESEND_API_KEY in .env and you'll get an actual email.

docker compose logs -f user-gateway

3. Become an admin

Add your email to the ADMIN_EMAILS variable in .env (comma-separated for multiple admins), then restart user-gateway:

docker compose restart user-gateway

The next time you sign in (or simply freshen your session), the admin flag is applied. Visit /admin — you're in.

Security note: ADMIN_EMAILS grants admin on login only. Removing an email from the list does not revoke admin. That's deliberate: rotating the env var can't lock you out of your own system. To revoke, set is_admin = false in Postgres directly.

Architecture

Four microservices + Caddy + Postgres + Redis. Caddy is the single HTTP entry point and routes each path to the service that owns it:

                         ┌─────────┐
                         │  Caddy  │  :80 (dev) / :443 (prod, auto TLS)
                         └────┬────┘
                              │
        ┌───────────┬─────────┼────────────┬─────────────┐
        │           │         │            │             │
        ▼           ▼         ▼            ▼             ▼
┌──────────────┬─────────────┬────────────┬────────────┬────────┐
│ user-gateway │user-ingress │  admin-ui  │project-web │ static │
│    Rust      │  Next.js    │  Next.js   │  Next.js   │ assets │
│              │             │            │            │        │
│   /api/*     │ /login      │  /admin*   │  /         │        │
│              │ /magic      │            │  /dash…    │        │
│              │ /profile*   │            │            │        │
└──────┬───────┴─────────────┴────────────┴────────────┴────────┘
       │
       │ forward user's cookies to /api/auth/verify
       │ for identity checks — no shared JWT_SECRET
       │ in downstream services
       ▼
┌─────────────────┐
│ Postgres + Redis│
└─────────────────┘

Caddy routes

/api/* → user-gateway:3001 /admin* → admin-ui:3100 /login, /magic → user-ingress:3200 /profile* → user-ingress:3200 /callback/* → user-ingress:3200 (future OAuth) / → project-web:3000 (catch-all)

Services

Rust · Core

user-gateway

Axum + SQLx API. Owns the users and admin_audit_log tables. Exposes /api/auth/*, /api/user/*, /api/admin/*.

Next.js · Optional

user-ingress

End-user auth + profile UI. /login, /magic, /profile. Reserved /callback/* for future OAuth.

Next.js · Optional

admin-ui

Admin panel: overview stats, user search, freeze/delete/impersonate, audit log.

Next.js · Replace

project-web

Your project-specific frontend. Ships with a landing page and a /dashboard example showing how to call /api/auth/verify. Replace this.

API endpoints (user-gateway)

MethodPathPurpose
POST/api/auth/requestSend a magic link
POST/api/auth/consumeConsume a magic link, issue session
GET/api/auth/verifyReturns {id, email, is_admin} or 401/403
POST/api/auth/logoutClear all session cookies
DELETE/api/user/deleteSelf-delete your account
GET/api/admin/meAdmin profile (used by admin-ui as guard)
GET/api/admin/statsUser counts
GET/api/admin/users?q&limit&offsetPaginated user list
GET/api/admin/users/:idUser detail
DELETE/api/admin/users/:idDelete user (guards: not self, not admin)
POST/api/admin/users/:id/freezeFreeze sessions
POST/api/admin/users/:id/unfreezeUnfreeze
POST/api/admin/users/:id/impersonateShort-TTL session as another user
GET/api/admin/audit?limit&offsetAppend-only audit log

Starting a new project from saassy

Fork the repo or use it as a template on GitHub. Then:

Checking who the user is (the one pattern that matters)

Any downstream service — Next.js server component, another backend, anything — checks identity like this:

// services/project-web/app/dashboard/page.tsx
import { redirect } from 'next/navigation'
import { serverApiFetch } from '../lib/api-fetch'

export default async function Dashboard() {
  const res = await serverApiFetch('/api/auth/verify')
  if (res.status === 401 || res.status === 403) {
    redirect('/login?next=/dashboard')
  }
  const me = await res.json()  // { id, email, is_admin }
  return <main>Hello, {me.email}</main>
}

serverApiFetch forwards incoming cookies to user-gateway. No shared secret. No JWT decoding on your end. If you freeze or delete the user from the admin panel, their next request fails within milliseconds.

For AI agents

SKILL.md is saassy's single-file integration guide targeted at AI coding agents. It explains how to consume the stack from a downstream project: the auth-check pattern, the env vars, the anti-patterns. Pipe it straight into your agent — no file on disk needed. Run from the root of the project you want to add saassy to:

Claude Code:

curl -fsSL https://saassy.xyz/SKILL.md \
  | claude "Add saassy to this project following the instructions above."

OpenAI Codex CLI:

curl -fsSL https://saassy.xyz/SKILL.md \
  | codex "Add saassy to this project following the instructions above."

Using the prebuilt images

The four service images are built and pushed to Docker Hub on every main push by the CI pipeline. For multi-arch support, images are built for linux/amd64 and linux/arm64.

docker pull tqdminc/user-gateway:latest
docker pull tqdminc/user-ingress:latest
docker pull tqdminc/admin-ui:latest
docker pull tqdminc/project-web:latest

Tags include latest, main, and short commit SHAs. Tag a release like v0.2.0 on GitHub and you also get v0.2.0 and 0.2 tags.