Security¶
FastSvelte ships with security built in, not left as an exercise. The codebase went through a full security audit in 2026 covering authentication, sessions, Google sign-in, billing webhooks and the API surface, and every finding was fixed or documented. In plain terms, here is what protects your app out of the box:
- Passwords hashed with Argon2id, the current best practice
- Session, reset, verification and invitation tokens stored only as hashes, so a database leak does not hand out working credentials
- Sign-in with Google protected against account takeover and forged logins
- Brute force and email abuse blocked by per-IP rate limits
- Strict security headers on every API response
- Dependencies patched and monitored for new vulnerabilities
The rest of this page explains each of these, and the production checklist covers what to set before you go live.
Passwords¶
Passwords are hashed with Argon2id (backend/app/util/hash_util.py), a modern memory-hard algorithm. Plaintext passwords are never stored.
Passwords must be 8 to 64 characters, enforced on signup, invitation accept and reset. See Changing the Password Policy to change the length or add your own rules.
Changing a password from the profile page requires the current one, so a stolen session cookie is not enough to take over the account. Accounts created through Google have no password, so they see an account panel instead of the form.
A password change also signs the user out on other devices (backend/app/service/password_service.py):
- Changed from the profile page: other sessions are revoked, the current one is kept.
- Reset with an emailed link: all sessions are revoked, including any an attacker is holding.
Sessions¶
- Tokens are 256-bit random values (
secrets.token_urlsafe(32)). - Only a SHA-256 hash of the token is stored server-side, so a database leak doesn't expose usable session tokens. Password-reset, email-verification and invitation tokens are stored the same way: the real token only ever travels in the emailed link.
- The cookie is HttpOnly (JavaScript can't read it), Secure outside
dev, and SameSitestrictoutsidedev(laxin dev). - Logout invalidates the session server-side; expired sessions are pruned by the cron job.
See Authentication for the full model.
CSRF¶
Session cookies use SameSite strict in production, so browsers refuse to attach them to requests started by other sites. This protection assumes the frontend and the API share a registrable domain (for example app.example.com and api.example.com). Deploying them on unrelated domains would require SameSite=None, which removes the protection entirely. If you must deploy cross-site, add CSRF tokens or an Origin check first.
Access control¶
Precedence-based roles (readonly < member < org_admin < sys_admin) gate every route via min_role_required(...). All business data is organization-scoped, so tenants are isolated. See Multi-Tenancy.
Google sign-in protections¶
Sign-in with Google is hardened against the two ways it's commonly abused:
- Account takeover by email. Google is only trusted to identify a user when it confirms the email address is verified. An unverified address is rejected, so nobody can link a Google login to someone else's existing account by claiming their email.
- Forged logins. The flow is bound to the browser that started it (a signed
statevalue plus a matching one-time cookie), so a sign-in link can't be crafted elsewhere and used to log someone into an attacker's account.
See Google OAuth for setup.
AI spend protection¶
AI usage is hard-capped by default: when an organization exhausts its allotment + credits, calls are blocked rather than silently billed. Overage requires turning on both an org setting and a system-level kill switch, neither enabled by default. See AI Usage & Credit Billing. This protects you from runaway model spend.
CORS & email verification¶
Allowed origins are configured per environment in backend/app/config/settings.py and tighten in production. Accounts must verify their email before they can log in.
Rate limiting¶
The abuse-prone public endpoints are rate limited per client IP, so one source can't hammer them:
| Endpoint | Limit |
|---|---|
POST /auth/login |
5 / 15 min |
POST /auth/signup, POST /auth/signup-org |
3 / hour |
POST /password/forgot |
3 / hour |
POST /auth/resend-verification |
10 / min |
The priority is the endpoints that send email to a user-supplied address (signup, forgot-password, resend-verification): left open they let an attacker burn your email spend and sender reputation regardless of how much traffic you have. Login is capped against credential stuffing. A breach returns the standard ErrorResponse with a Retry-After header.
To rate limit another route, add the dependency to it:
from fastapi import Depends
from app.util.rate_limit import rate_limit
@router.post("/expensive", dependencies=[Depends(rate_limit("10/minute"))])
Storage (read before scaling)¶
Counters live in process memory by default (FS_RATE_LIMIT_STORAGE_URI=async+memory://). The shipped container runs a single process, so this is correct for one instance. But each instance keeps its own counters, so if you run more than one instance (horizontal scaling / replicas behind a load balancer) the effective limit multiplies by the number of instances. For a real limit across instances, point it at a shared store:
Async Redis needs the coredis package (uv add coredis).
Behind a proxy¶
The limiter reads the client IP from X-Forwarded-For, which hosting platforms set for you. If you'd rather rate limit at the edge, a reverse proxy like nginx can do it too. See its limit_req docs.
Deliberately left out¶
Kept out to stay lean; add if your threat model calls for it:
- Per-account login limiting (keying login on the target email, not just the IP) defends a distributed attack against one account. Worth adding once you have accounts worth attacking.
- The AI endpoint is authenticated, so it's not a public abuse surface, and it's already hard-capped by the AI spend protection above. It gets no separate HTTP limit.
- Reset and verify token endpoints rely on high-entropy tokens rather than a request limit.
Dependencies¶
Every dependency is pinned to an exact version (uv.lock, package-lock.json), so builds are reproducible and nothing updates without you noticing. The stack ships with zero known vulnerabilities at release, and the repository has automated security alerts turned on, so newly disclosed issues surface as ready-to-merge fix requests. Keeping current is a routine, not a scramble.
HTTP security headers¶
FastSvelte protects API responses by default and provides a ready-to-use header policy for your frontend.
API: already protected¶
FastSvelte adds these headers to every API response, including errors:
- No caching: private API data is not stored in browser or shared caches.
- No content guessing: browsers cannot treat JSON as HTML or another unsafe type.
- No embedding: API responses cannot be displayed inside another website.
- No referrer leakage: URLs and IDs are not passed to third-party sites.
No setup is required. These are set in backend/app/api/middleware/security_headers.py.
Frontend: add at your host¶
Your frontend host, CDN, or reverse proxy must send these headers for the app. They help ensure the app loads only trusted resources, stays HTTPS-only, cannot be embedded by another site, and does not enable unused browser features.
The headers, and where to put them, are in the guide for your host: Vercel, Railway, Azure, DigitalOcean, self-hosting.
Why the policy allows inline styles
The policy sets style-src 'self' 'unsafe-inline'. Svelte animations inject an inline <style> element at runtime, so transitions break without it. This applies to styles only. Scripts stay limited to your own domain, which is where the real risk lies.
Check after deploy¶
curl -sI https://app.yourdomain.com | grep -i "content-security\|x-content-type\|referrer\|strict-transport"
curl -sI https://api.yourdomain.com/ping | grep -i "content-security\|x-content-type"
If the app does not load correctly, check the browser console for CSP errors. Most often, the API URL is missing from connect-src.
Production checklist¶
- Strong, unique
FS_JWT_SECRET_KEYandFS_CRON_SECRET. - Serve over HTTPS so
Securecookies take effect. - Strict
FS_CORS_ORIGINSfor production. - Stripe live keys + verified webhook secret (see Billing & Subscriptions).
- Add the frontend security headers at your host, with your real API URL in
connect-src.