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.
sqlx::migrate!, so there's no separate migrate
service.
GET /api/auth/verify. Revocation
is instant because user-gateway loads the user on every verify.
SITE_ADDRESS
to your domain and Caddy auto-provisions Let's Encrypt certs.
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
Open http://localhost (or whatever CADDY_HTTP_PORT you set):
/ — project-web landing page/login — magic-link sign in/dashboard — auth-gated example (redirects to /login if not signed in)/profile — your account page/admin — admin panel (admins only)
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
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.
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.
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│
└─────────────────┘
Axum + SQLx API. Owns the users and
admin_audit_log tables. Exposes /api/auth/*,
/api/user/*, /api/admin/*.
End-user auth + profile UI. /login, /magic, /profile. Reserved /callback/* for future OAuth.
Admin panel: overview stats, user search, freeze/delete/impersonate, audit log.
Your project-specific frontend. Ships with a landing page and a /dashboard example showing how to call /api/auth/verify. Replace this.
| Method | Path | Purpose |
|---|---|---|
| POST | /api/auth/request | Send a magic link |
| POST | /api/auth/consume | Consume a magic link, issue session |
| GET | /api/auth/verify | Returns {id, email, is_admin} or 401/403 |
| POST | /api/auth/logout | Clear all session cookies |
| DELETE | /api/user/delete | Self-delete your account |
| GET | /api/admin/me | Admin profile (used by admin-ui as guard) |
| GET | /api/admin/stats | User counts |
| GET | /api/admin/users?q&limit&offset | Paginated user list |
| GET | /api/admin/users/:id | User detail |
| DELETE | /api/admin/users/:id | Delete user (guards: not self, not admin) |
| POST | /api/admin/users/:id/freeze | Freeze sessions |
| POST | /api/admin/users/:id/unfreeze | Unfreeze |
| POST | /api/admin/users/:id/impersonate | Short-TTL session as another user |
| GET | /api/admin/audit?limit&offset | Append-only audit log |
Fork the repo or use it as a template on GitHub. Then:
services/user-gateway,
services/user-ingress, services/admin-ui,
infra/caddy/, docker-compose.yml. These are the
reusable bits.
services/project-web/ with your own
Next.js app (or swap it out for Astro / SvelteKit / whatever — it's just a
container behind Caddy).
http://user-gateway:3001/api/auth/verify.
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.
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."
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.