Skip to main content

A par-baked starter for PoC apps: signup-with-approval auth, rate limiting, and deploy scaffolding for fly.io

Project description

parbaked

A par-baked starter for FastAPI PoCs. Signup with admin approval, rate limiting, an admin dashboard, and a published off-boarding contract — set up in two commands.

If you ship PoCs on fly.io, two things eventually bite you: anyone on the internet can spin up accounts in a loop (and run up your bill), and you write the same auth boilerplate every time. parbaked is the slice between "I have an idea" and "this is safe to put online."

30-second quickstart

uvx parbaked new myapp
cd myapp
parbaked dev

Open http://localhost:8000/auth/admin and log in as admin with the password from the terminal banner. The admin dashboard, signup/login REST API, and rate-limit middleware are already wired. Drop more .py files in routes/ and they auto-mount.

You now have a working PoC with:

  • Signup + login + JWT sessions
  • Admin-approval gate (nobody gets in until you click approve)
  • Built-in admin dashboard with pending/active/rejected queues
  • Per-IP rate limiting (5/min signup, 10/min login by default)
  • SQLite DB on a fly volume (persists across machine restarts)
  • Secrets auto-generated on first run, never committed

What parbaked new scaffolds

myapp/
├── parbaked.toml          # config
├── routes/
│   └── index.py           # add more .py files, they auto-mount
├── models.py              # your SQLModel tables go here
├── pyproject.toml
├── .env.example
├── .gitignore
└── README.md

No main.py. No Dockerfile. No fly.toml. The kernel runtime owns the entrypoint; parbaked deploy generates the deploy targets from parbaked.toml. If you ever want to take ownership, parbaked eject hands you everything (see below).

Adding routes

Drop a .py file in routes/ with a module-level router:

# routes/notes.py
from fastapi import APIRouter, Depends
from parbaked import current_user

router = APIRouter()

@router.get("/")
def list_notes(user = Depends(current_user)):
    return {"user": user.email, "notes": []}

Restart the dev server (or wait for hot reload) and GET /notes/ returns your handler's response. The auto-discovery rule: file path becomes URL prefix. routes/api/users.py mounts at /api/users. routes/index.py mounts at /.

Shipping to fly.io

parbaked deploy

That's it. parbaked generates a Dockerfile + fly.toml into .parbaked/build/ from your parbaked.toml, creates the SQLite volume if it doesn't exist, and runs fly deploy. The generated fly.toml ships with cost-protection defaults — auto_stop_machines = "stop", min_machines_running = 0, max_machines_running = 2 — so an idle deploy costs nothing and a traffic spike can't autoscale you into a bill.

For first-time setup:

fly launch --copy-config --no-deploy   # claims the app name
parbaked secrets                       # pushes .env to fly secrets
parbaked deploy

Tunnel for local sharing: parbaked tunnel spins up a cloudflared quick tunnel — a *.trycloudflare.com URL with no Cloudflare account needed.

Off-boarding contract

Every byte parbaked owns is in a public, importable format:

  • Password hashes: bcrypt 2b, cost 12 — verifiable with any bcrypt-compliant library.
  • Session tokens: RFC 7519 JWT, HS256 — decodable on jwt.io or any stdlib.
  • Users table: documented column-by-column in docs/data-format-guarantees.md. Additive changes only between major versions.
  • Audit events: one JSON object per stdout line — ships to any log aggregator.

When you outgrow parbaked, run parbaked eject. You get a parbaked-export/ directory: PostgreSQL-compatible schema.sql, CSV data dumps, the generated Dockerfile + fly.toml, an .env.example listing the env-var contract, and a MANIFEST.md pointing back at the format docs. No vendor-specific decoders. No lobster trap.

Configuration

Everything has sensible defaults. Set in parbaked.toml (non-secret) or via PARBAKED_* env vars (secrets):

Env var Default What it does
PARBAKED_JWT_SECRET (auto-generated) Session-token signing key. Required in prod.
PARBAKED_APPROVAL_TOKEN_SECRET (auto-generated) Magic-link signing key. Required in prod.
PARBAKED_ADMIN_PASSWORD (auto-generated) Dashboard login password (user is always admin).
PARBAKED_ADMIN_EMAIL unset Where signup-approval emails go (dashboard works without this).
PARBAKED_APP_NAME "My App" Used in email subjects.
PARBAKED_APP_URL http://localhost:8000 Public URL for magic links.
PARBAKED_RESEND_KEY Set this to send real email via Resend. Unset → emails print to stdout. Get a free key (no card) at https://resend.com/api-keys or run parbaked email setup.
PARBAKED_MAIL_FROM onboarding@resend.dev From address. Default is Resend's sandbox. For real users, verify your domain at resend.com/domains.
PARBAKED_RATELIMIT_SIGNUP 5/minute Per-IP signup limit.
PARBAKED_RATELIMIT_LOGIN 10/minute Per-IP login limit.
PARBAKED_DATABASE_URL sqlite:///./parbaked.db SQLite-only. Standard SQLAlchemy URL.

Auto-generated secrets get persisted to .parbaked.json (chmod 600). In production, set them as env vars instead.

Optional: Sign in with Google

parbaked's default path is password + admin approval. No third-party signup needed. Google sign-in is opt-in and only worth turning on when your users expect to see that button.

Set PARBAKED_GOOGLE_CLIENT_ID and PARBAKED_GOOGLE_CLIENT_SECRET and parbaked exposes /auth/google/start + /auth/google/callback. Create the OAuth client in Google Cloud Console with https://your-app.example.com/auth/google/callback as an authorized redirect URI. The approval gate still applies — Google-authenticated users land in the same pending queue.

Security posture

What protects you from a bill:

  • Per-IP rate limits on signup, login, and password reset (slowapi).
  • No email enumeration — signup with an existing email and login with a wrong password return generic errors.
  • Approval gate — even if someone gets past the rate limit, they can't do anything until you click approve.
  • bcrypt for passwords, HS256 JWT for sessions, audience-scoped JWT for magic links so a session token can never be replayed as approval (and vice versa).
  • CSRF double-submit cookies on every admin POST.
  • Production-mode tripwiresPARBAKED_ENV=production refuses to boot if any required secret was auto-generated, warns on multi-instance setups, suppresses the admin password in the banner.

Running under a different ASGI server

parbaked dev shells out to uvicorn parbaked.runtime:create_app --factory. For Gunicorn / Hypercorn / Granian, write a 2-line wsgi.py:

from parbaked.runtime import create_app
app = create_app()

Then gunicorn -k uvicorn.workers.UvicornWorker wsgi:app --workers 4. See AGENTS.md for the full advanced section.

Install

pip install parbaked
# or
uv add parbaked

Requires Python 3.11+.

Develop / contribute

git clone https://github.com/saml7n/parbaked
cd parbaked
uv venv && source .venv/bin/activate
uv pip install -e ".[dev]"
pytest

Issues and PRs welcome.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

parbaked-1.0.0.tar.gz (70.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

parbaked-1.0.0-py3-none-any.whl (92.9 kB view details)

Uploaded Python 3

File details

Details for the file parbaked-1.0.0.tar.gz.

File metadata

  • Download URL: parbaked-1.0.0.tar.gz
  • Upload date:
  • Size: 70.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for parbaked-1.0.0.tar.gz
Algorithm Hash digest
SHA256 80ca70da9502471d4480eb3d65ad1989731900f2e17755fde165266cd5700d85
MD5 92a23f733b2de5d823f2665adfc820cd
BLAKE2b-256 ad40aa2f06b889a753bcae4332e9f3e1b133102212343ecc859e834d3ce2cce7

See more details on using hashes here.

Provenance

The following attestation bundles were made for parbaked-1.0.0.tar.gz:

Publisher: release.yml on saml7n/parbaked

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file parbaked-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: parbaked-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 92.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for parbaked-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d1347d9a9131892a83e8533d76d73debe82256f267d9357d9868c1730e488615
MD5 f52e064f396a93db748ce4a0fd09a446
BLAKE2b-256 63ba18b0a5617578be1a4535a20018fbd429db6dcaed9c010fdde93274de1c75

See more details on using hashes here.

Provenance

The following attestation bundles were made for parbaked-1.0.0-py3-none-any.whl:

Publisher: release.yml on saml7n/parbaked

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page