Skip to main content

A lightweight Postgres-first BaaS framework for Python

Project description

supython

A lightweight, Postgres-first BaaS framework for Python. v0.1.2 release

the database owns the schema, Python owns the things SQL is bad at. It leans on PostgREST for auto-generated REST APIs and on Postgres' own RLS for authorization, while a small FastAPI service in Python handles auth, JWT issuance, realtime, storage, functions, workers, and an optional admin control plane.

supython is for a specific person with a specific problem:

A developer who wants to build a CRUD-heavy web app (most apps are), who thinks in SQL, who wants Postgres to own authorization, and who wants auth + storage + custom logic without assembling the integration themselves.

Shipped [v0.1.2]:

Core platform

  • Email/password auth — signup, login, refresh-token rotation with reuse detection
  • OAuth (Google + GitHub) via authlib with PKCE
  • Password reset, magic link, and email OTP (pluggable email backend)
  • RS256 JWT — asymmetric signing; PostgREST verifies via shared JWKS; zero-downtime key rotation
  • Rate limiting on auth endpoints (per-IP fixed-window counters)
  • Row-Level Securityauth.uid() helper, request.jwt.claims GUC, role-scoped DB access via db.as_role()
  • supython init — scaffold a new project in one command
  • supython gen types --lang py — emit typed dataclasses + TypedDicts from your Postgres schema
  • zero Python CRUD code — every read, write, filter, sort, and pagination is served by PostgREST under RLS

Storage & functions

  • S3/local storage with RLS-on-metadata, signed URLs, multipart upload, and range download
  • Edge functions from a functions/ directory with hot reload and role-scoped DB access

Realtime

  • WebSocket Realtimepostgres_changes, broadcast, presence with per-subscriber RLS filtering
  • Phoenix Channels wire format
  • Generic triggerrealtime.enable('public.todos') opts any table in
  • Two-browser chat demoexamples/chat.html (zero build step)

Jobs & cron

  • Job queue — Postgres-backed (SELECT FOR UPDATE SKIP LOCKED), idempotent enqueue, retry with backoff
  • Cron schedulingpg_cron (primary) or in-process croniter fallback
  • Generic hooks@app.on_signup / @app.on_login lifecycle hooks; @app.claims_provider for custom JWT claims
  • supython worker run — long-running worker with graceful SIGTERM drain

Operations & security

  • Structured JSON logs — request-id propagation, redacted auth headers, tracebacks on 5xx
  • /livez / /readyz / /health — liveness, dependency readiness (DB, PostgREST, broker, worker), full detail
  • Security headers — HSTS, CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy
  • supython doctor — diagnoses roles, extensions, grants, JWKS, migration drift, symmetric secrets
  • Secret rotation — JWT keys, symmetric secrets, Postgres passwords; all with zero-downtime runbooks
  • Multi-arch Docker imagelinux/amd64 + linux/arm64, non-root user, tini PID 1, ~64 MB

Admin control plane (shipped in v0.1.2)

  • Vue 3 + Vite SPA at /admin — no runtime Node deps; pre-built static bundle in the wheel
  • Database surface — schema browser, table data with role switcher, SQL workspace (read-only default + write toggle), RLS policy editor with dry-run, migrations panel
  • Auth surface — user search, ban/unban/force-logout, refresh-token inspector, audit log, email template editing
  • Module screens — storage buckets/objects, function routes/invoke, realtime tables/channels, job queue/crons
  • Ops — backup management, live log tail via SSE with level/request-id filters
  • supython admin create-user — bootstrap the first admin (no chicken-and-egg)

Architecture

              ┌──────────────────────┐
client ─────► │  supython (FastAPI)  │  /auth/v1/*, /storage/v1/*,
              │   port 8000          │  /functions/v1/*, /jobs/v1/*,
              │                      │  /admin, /livez, /readyz  ──┐
              └──────────────────────┘                             │
                         │                                         │
              ┌──────────────────────┐                             │
client ─────► │  PostgREST            │  /<table>                 ─┤
              │   port 54321          │                             │
              └──────────────────────┘                             │
                         │                                         │
                         ▼                                         │
              ┌──────────────────────┐ ◄───────────────────────────┘
              │      Postgres        │
              │   port 54322         │
              │  roles: anon /       │
              │    authenticated /   │
              │    service_role      │
              └──────────────────────┘

The unifying contract is the JWT + Postgres role system. supython mints the JWT; PostgREST verifies the signature via shared JWKS and runs every request under the role + claims it carries. RLS does the rest.

Quick start

Requires Python 3.11+, Docker 24+ with the compose plugin.

# 1. install the wheel
python -m venv .venv && source .venv/bin/activate
pip install supython

# 2. scaffold a new project
supython init myapp
cd myapp
cp .env.example .env
# Review .env — at minimum confirm AUTHENTICATOR_PASSWORD matches
# the value PostgREST will use (docker-compose.yml injects it via env).
#
# The scaffold creates:
#   manage.py         — Optional CLI entrypoint (sets SUPYTHON_SETTINGS_MODULE)
#   myapp/settings.py — declare EXTENSIONS, EXTRA_ROUTERS, EXTRA_MIDDLEWARE
#   myapp/jobs.py     — example @job seed (register your background jobs here)
#   myapp/hooks.py    — example @on("signup") seed (lifecycle hooks)
#   myapp/asgi.py     — optional entrypoint for uvicorn/gunicorn

# 3. boot Postgres + PostgREST and run migrations (one command)
supython up

# 4. run the auth/API service (separate terminal)
supython dev

# 5. (optional) generate typed Python classes from your Postgres schema
supython gen types --lang py --out db_schema.py

# 6. (optional) bootstrap the admin dashboard
supython admin create-user
# then open http://localhost:8000/admin

You should now have:

service url
supython http://localhost:8000
Admin UI http://localhost:8000/admin
PostgREST http://localhost:54321
Postgres postgres://supython:supython@localhost:54322/supython

End-to-end smoke test

# sign up
curl -sS -X POST http://localhost:8000/auth/v1/signup \
  -H 'content-type: application/json' \
  -d '{"email":"alice@example.com","password":"password123"}'

# get a fresh token
TOKEN=$(curl -sS -X POST http://localhost:8000/auth/v1/token \
  -H 'content-type: application/json' \
  -d '{"email":"alice@example.com","password":"password123"}' \
  | python -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')

# create a todo via PostgREST — note: NO Python code involved
curl -sS -X POST http://localhost:54321/todos \
  -H "authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -H 'prefer: return=representation' \
  -d '{"title":"buy milk"}'

# list todos — RLS hides everyone else's rows
curl -sS http://localhost:54321/todos -H "authorization: Bearer $TOKEN"

See examples/todos.http for the full set of calls including filtering, sorting, refresh, and isolation between users.

Realtime quickstart

supython ships a WebSocket engine that speaks the Phoenix Channels 5-tuple protocol.

1. Opt a table into realtime

-- run once (or add to a migration)
SELECT realtime.enable('public.messages');
-- with a custom owner column for DELETE visibility:
-- SELECT realtime.enable('public.messages', 'author_id');

Or via CLI (requires a running server):

supython realtime enable public.messages

2. Open the demo

# start the stack
supython up
supython dev          # in a second terminal

# open the chat demo in two browser tabs
python -m http.server --directory examples 8080
# → http://localhost:8080/chat.html

Enter your JWT (or leave blank for anon role), pick a room name, and open the same page in a second tab. Messages broadcast instantly; Postgres row changes appear as structured cards.

3. Subscribe from JavaScript (no SDK required)

// Phoenix Channels 5-tuple: [join_ref, ref, topic, event, payload]
const ws = new WebSocket(
  "ws://localhost:8000/realtime/v1/websocket?apikey=<JWT>&vsn=1.0.0"
);

let ref = 0;
ws.onopen = () => {
  ws.send(JSON.stringify(["1", String(++ref), "realtime:room-42", "phx_join", {
    config: {
      postgres_changes: [{ event: "*", schema: "public", table: "messages" }],
      broadcast: { self: false }
    },
    access_token: "<JWT>"
  }]));
};

ws.onmessage = ({ data }) => {
  const [, , topic, event, payload] = JSON.parse(data);
  if (event === "postgres_changes") console.log(payload.data);
  if (event === "broadcast")        console.log(payload.payload);
};

4. Subscribe from Python

import asyncio, json
import websockets

TOKEN = "eyJ..."  # or omit for anon

async def main():
    url = f"ws://localhost:8000/realtime/v1/websocket?apikey={TOKEN}&vsn=1.0.0"
    async with websockets.connect(url) as ws:
        await ws.send(json.dumps([
            "1", "1", "realtime:room-42", "phx_join",
            {"config": {"broadcast": {"self": True}}, "access_token": TOKEN}
        ]))
        async for raw in ws:
            join_ref, ref, topic, event, payload = json.loads(raw)
            print(event, payload)

asyncio.run(main())

5. REST broadcast (server → clients)

curl -sS -X POST http://localhost:8000/realtime/v1/broadcast/room-42 \
  -H "authorization: Bearer $SERVICE_ROLE_TOKEN" \
  -H "content-type: application/json" \
  -d '{"event": "announcement", "payload": {"text": "Hello from the server!"}}'

Realtime settings (.env)

Variable Default Purpose
REALTIME_ENABLED true Toggle the realtime module
REALTIME_NOTIFY_CHANNEL realtime:changes Postgres LISTEN channel
REALTIME_MAX_CONNECTIONS 1000 Max concurrent WS clients
REALTIME_MAX_SUBS_PER_CONN 100 Max channel joins per connection
REALTIME_HEARTBEAT_TIMEOUT_SECONDS 30 Idle-close timeout (client sends every 25s)
REALTIME_BROKER_QUEUE_SIZE 1000 Per-subscriber outbound queue depth

Jobs & cron quickstart

1. Define a job

# in your application code
from supython.jobs.decorators import job

@job("send_welcome_email", version=1, max_attempts=5)
async def send_welcome_email(ctx, payload):
    await ctx.send_email(
        to=payload["email"],
        subject="Welcome!",
        text="Thanks for signing up.",
    )

2. Enqueue from a function or hook

from supython import hooks
from supython.jobs.service import enqueue

@hooks.on("signup")
async def on_signup(user, ctx):
    await enqueue(
        ctx.db,
        name="send_welcome_email",
        payload={"email": user.email},
        idempotency_key=f"welcome:{user.id}",
    )

3. Schedule a cron

from supython.jobs.decorators import cron

@cron("*/5 * * * *", name="cleanup", job_name="cleanup_job")
async def cleanup_job(ctx, payload):
    ctx.logger.info("running periodic cleanup")

4. Run the worker

supython worker run --queue default --concurrency 5

Or enable in-process mode for development:

# in .env
JOBS_DEV_INPROCESS=true
supython dev

Jobs settings (.env)

Variable Default Purpose
JOBS_ENABLED true Toggle the jobs module
JOBS_BACKEND pg Queue backend (only pg for now)
JOBS_CRON_BACKEND pg_cron pg_cron, inproc, or off
JOBS_POLL_INTERVAL_S 1.0 Seconds between queue polls
JOBS_CONCURRENCY 5 Max concurrent jobs per worker
JOBS_DEFAULT_MAX_ATTEMPTS 3 Default retry limit
JOBS_BACKOFF_BASE_S 5.0 Base backoff delay (seconds)
JOBS_BACKOFF_MAX_S 300.0 Max backoff delay (seconds)
JOBS_VISIBILITY_TIMEOUT_S 300.0 Zombie reclaim timeout (seconds)
JOBS_DRAIN_TIMEOUT_S 30.0 Graceful shutdown drain (seconds)
JOBS_DEV_INPROCESS false Spawn worker in-process during supython dev

v0.2 auth endpoints

Endpoint Method Purpose
/auth/v1/signup POST Create account, return token pair
/auth/v1/token POST Password login
/auth/v1/refresh POST Rotate refresh token (reuse detection built in)
/auth/v1/logout POST Revoke refresh token
/auth/v1/user GET Return the caller's user (JWT required)
/auth/v1/recover POST Request password-reset email
/auth/v1/recover/verify POST Verify reset token, set new password
/auth/v1/magiclink POST Request magic-link email
/auth/v1/magiclink/verify GET Verify magic-link token (?token=…)
/auth/v1/otp POST Request email OTP
/auth/v1/otp/verify POST Verify OTP code
/auth/v1/authorize/{provider} GET Start OAuth flow (redirect to provider)
/auth/v1/callback/{provider} GET Handle OAuth callback, redirect with tokens

Email backend — set EMAIL_BACKEND=console (default, logs to stdout) or EMAIL_BACKEND=smtp and configure SMTP_HOST / SMTP_PORT / SMTP_USERNAME / SMTP_PASSWORD.

OAuth — add GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET (and/or GitHub equivalents) to .env. Providers without credentials are silently disabled.

Custom JWT claims

Register a claims_provider to inject application-specific claims into every access token minted by the auth endpoints. Each provider is an async callable (user, conn) -> dict whose return value is merged into the token payload — on signup, password login, refresh, magic-link, OTP, and OAuth callbacks.

from supython.app import app

@app.claims_provider
async def add_org(user, conn):
    org_id = await conn.fetchval(
        "select org_id from public.memberships where user_id = $1", user.id
    )
    return {"org_id": str(org_id)} if org_id else {}

Notes:

  • Reserved JWT claims (sub, email, role, aud, iat, exp, jti) cannot be overridden — they are filtered out automatically.
  • Providers run on the service-role connection used by the auth flow, so they can read tables that RLS would block during issuance. Treat the function as privileged code (same posture as a Postgres security definer routine — sanitize any user-supplied input).
  • A provider that raises aborts issuance: a missing claim is a silent authz bug, not a missing welcome email.
  • Refresh re-collects claims, so a token rotated via /auth/v1/refresh reflects current state.

Auth hardening settings (.env)

Variable Default Purpose
DB_STATEMENT_TIMEOUT_MS 30000 Per-connection query timeout for the asyncpg pool (0 disables)
DB_POOL_MIN_SIZE 1 Minimum asyncpg pool size
DB_POOL_MAX_SIZE 10 Maximum asyncpg pool size
AUTH_RATE_LIMIT_ENABLED true Toggle auth endpoint rate limiting
AUTH_RATE_LIMIT_WINDOW_SECONDS 60 Fixed-window size for auth endpoint counters
AUTH_RATE_LIMIT_TOKEN_PER_WINDOW 10 /auth/v1/token attempts per IP/window
AUTH_RATE_LIMIT_SIGNUP_PER_WINDOW 5 /auth/v1/signup attempts per IP/window
AUTH_RATE_LIMIT_RECOVER_PER_WINDOW 3 /auth/v1/recover attempts per IP/window
AUTH_RATE_LIMIT_OTP_PER_WINDOW 5 /auth/v1/otp attempts per IP/window
AUTH_RATE_LIMIT_MAGICLINK_PER_WINDOW 5 /auth/v1/magiclink attempts per IP/window

AUTHENTICATOR_PASSWORD — the password used for the authenticator Postgres role that PostgREST connects as. Defaults to authenticator (matches the migration), but you should change it in production. supython up automatically runs ALTER ROLE authenticator WITH PASSWORD … after migrations so you never need to edit SQL.

What's in the box

 supython/
 ├── manage.py                       # optional cli entrypoint (sets SUPYTHON_SETTINGS_MODULE)
 ├── docker-compose.yml              # Postgres + PostgREST (dev stack)
 ├── docker-compose.prod.yml         # hardened single-host production stack
 ├── docker-compose.test.yml         # dedicated test Postgres on port 54323
 ├── Caddyfile                       # reverse-proxy TLS for prod
 ├── <name>/                         # your Python package
 │   ├── __init__.py
 │   ├── settings.py                 # project settings (EXTENSIONS, EXTRA_ROUTERS, …)
 │   ├── asgi.py                     # optional ASGI entrypoint (uvicorn <name>.asgi:app)
 │   ├── jobs.py                     # example @job seed
 │   └── hooks.py                    # example @on("signup") seed
 ├── migrations/
 │   ├── 0001_extensions_and_roles.sql      # anon/authenticated/service_role/authenticator
 │   ├── 0002_auth_schema.sql               # auth.users, auth.refresh_tokens, auth.uid()
 │   ├── 0003_demo_todos.sql                # the demo table + RLS policies
 │   ├── 0004_auth_v0_2.sql                # identities, one_time_tokens, audit_log
 │   ├── 0005_storage_schema.sql            # storage.buckets + storage.objects
 │   ├── 0006_realtime_schema.sql           # realtime schema, trigger, enable() helper
 │   ├── 0007_jobs_schema.sql               # jobs queue, cron_schedules, enqueue/claim_next
 │   ├── 0008_jobs_last_error.sql           # last_error column on jobs.jobs
 │   ├── 0009_auth_rate_limits.sql          # auth fixed-window rate-limit counters
 │   ├── 0010_worker_heartbeat.sql          # worker heartbeat for /readyz
 │   ├── 0011_admin_schema.sql              # admin.admin_users, sessions, audit
 │   ├── 0012_auth_banned_until.sql         # banned_until on auth.users
 │   ├── 0013_email_templates.sql            # auth.email_templates
 │   ├── 0014_realtime_payload_warning.sql   # >8KB payload warning counter
 │   └── 0015_backups_schema.sql             # backups metadata
 ├── examples/
 │   ├── todos.http                  # HTTP smoke tests (auth + PostgREST)
 │   ├── storage.http                # storage upload / signed URL examples
 │   ├── functions.http              # edge-function call examples
 │   └── chat.html                   # two-browser realtime demo (vanilla JS, zero deps)
 ├── docs/
 │   ├── PROJECT.md                  # architecture + roadmap (single source of truth)
 │   ├── Installation.md             # full install guide (dev, prod, managed Postgres)
 │   └── admin-ui/
 │       ├── admin-surface-plan.md   # admin implementation plan + phase status
 │       └── admin-surface.md        # admin architecture + contracts
 ├── tests/
 │   ├── conftest.py                 # cross-tree fixtures (keys, capturing mailer)
 │   ├── _keys.py                    # JWT-forging helpers
 │   ├── fixtures/                   # test function modules, etc.
 │   ├── unit/                       # pure-Python tests (~6s, no Docker)
 │   │   ├── test_admin_session.py
 │   │   ├── test_admin_service_db.py
 │   │   └── ...
 │   └── integration/                # full ASGI + Postgres on port 54323
 │       ├── conftest.py             # pool, app, client, autouse DB cleaners
 │       ├── test_auth_signup_login.py
 │       ├── test_admin_auth.py
 │       ├── test_admin_auth_users.py
 │       ├── test_admin_db_rows.py
 │       ├── test_admin_jobs.py
 │       ├── test_admin_ops_backups.py
 │       ├── test_admin_storage.py
 │       ├── test_postgrest_rls.py
 │       ├── test_realtime_ws.py
 │       └── ...
 ├── admin-ui/                       # Vue 3 + Vite SPA (built → src/supython/admin/static/)
 │   └── src/
 │       ├── api/                    # single fetch seam
 │       ├── components/             # shell, data, editors, feedback
 │       ├── composables/            # useResource, useTable, useConfirm, useImpersonate, …
 │       ├── stores/                 # auth, ui
 │       ├── views/                  # Dashboard, db/, auth/, storage/, functions/, …
 │       └── router/
 └── src/supython/
     ├── __init__.py                 # single version string
     ├── settings.py                 # pydantic-settings, .env-driven
     ├── db.py                       # asyncpg pool + lifespan + as_role() / as_service_role()
     ├── mailer.py                   # ConsoleBackend / SmtpBackend
     ├── tokens.py                   # RS256/ES256 JWT + JWKS
     ├── passwords.py                # argon2id
     ├── migrate.py                  # ~50-line SQL migration runner
     ├── app.py                      # FastAPI factory
     ├── cli.py                      # typer: up, dev, keygen, admin, worker, test, …
     ├── extensions.py               # eager-import dotted module paths at boot
     ├── settings_module.py          # user settings (EXTENSIONS, EXTRA_ROUTERS, …)
     ├── health.py                   # /livez, /readyz, /health endpoints
     ├── logging_config.py           # structured JSON log setup
     ├── security_headers.py         # HSTS, CSP, etc.
     ├── body_size.py                # request body size guards
     ├── jwks.py                     # JWKS generation + rotation helpers
     ├── keyset.py                   # asymmetric key rotation manifest
     ├── secretset.py                # symmetric secret rotation manifest
     ├── hooks.py                    # generic hook system: on() / fire()
     ├── mail.py                     # email send with job-retry fallback
     ├── auth/
     │   ├── schemas.py
     │   ├── service.py              # full auth layer: signup / OAuth / OTP / recover …
     │   ├── router.py               # all /auth/v1/* routes
     │   └── providers/              # Google, GitHub, OAuth2 helpers
     ├── storage/
     │   ├── backends.py             # LocalBackend, S3Backend
     │   ├── service.py
     │   └── router.py               # /storage/v1/*
     ├── functions/
     │   ├── loader.py               # filesystem discovery + hot reload
     │   └── router.py               # /functions/v1/*
     ├── realtime/
     │   ├── protocol.py             # Phoenix Channels encode/decode
     │   ├── broker.py               # fan-out engine with RLS filtering
     │   ├── websocket.py            # WS route with JWT auth
     │   └── router.py               # /realtime/v1/*
     ├── jobs/
     │   ├── registry.py             # @job / @cron decorator store
     │   ├── service.py              # enqueue, claim_next, mark_*
     │   ├── worker.py               # long-running poll/dispatch/drain loop
     │   ├── cron.py                 # pg_cron sync + InProcScheduler
     │   └── router.py               # /jobs/v1/*
     ├── admin/
     │   ├── session.py              # admin cookie session (SHA-256 hashed, 8h TTL)
     │   ├── deps.py                 # require_admin dependency
     │   ├── spa.py                  # static SPA mount + index.html fallback
     │   ├── schemas.py              # shared Pydantic models
     │   ├── audit.py                # admin audit log writer
     │   ├── static/                 # pre-built Vue 3 SPA bundle (committed)
     │   └── api/                    # /admin/api/v1/* route handlers
     ├── backups/                    # pg_dump wrapper + restore
     ├── gen/                        # supython gen types --lang py|ts
     ├── scaffold/                   # supython init templates
     └── client/                     # Python SDK (optional [client] extra)

Plugins & extensions

supython uses a settings module to declare your app's extensions:

# <name>/settings.py — scaffolded by `supython init`

EXTENSIONS = [
    "myapp.jobs",    # your @job / @cron decorators
    "myapp.hooks",   # your @on("signup") / @on("login") hooks
]

EXTRA_ROUTERS: list[str] = []       # "module.path:router_symbol"
EXTRA_MIDDLEWARE: list[str] = []    # "module.path:ClassName"

The scaffolded manage.py sets SUPYTHON_SETTINGS_MODULE so the CLI and worker automatically discover your extensions. You can also set EXTENSIONS=myapp.jobs,myapp.hooks directly in .env if you prefer not to use a settings module.

Extensions are plain Python modules imported eagerly at boot — their @job, @cron, and @on decorators register before the FastAPI app or worker starts. This is the same mechanism supython's own internals use to stay composable without cross-module imports.

CLI

supython up                 # docker compose up + migrate + start postgrest
supython up --prod          # boot the production stack
supython up --prod --worker # boot prod stack + worker
supython dev                # uvicorn the FastAPI service with reload
supython down               # stop the stack (keeps data)
supython down --prod        # stop the prod stack
supython reset              # stop the stack and DELETE the volume (destructive)
supython reset --prod       # stop prod stack and DELETE volumes
supython migrate            # apply pending SQL migrations
supython info               # print resolved settings
supython doctor             # diagnose roles, extensions, JWKS, grants, migration drift
supython init <name>        # scaffold a new supython project
supython gen types --lang py --out db_schema.py   # emit typed dataclasses + TypedDicts
supython gen types --lang ts --out types.ts   # emit TypeScript Database interface

# Auth & key management
supython keygen init [--alg RS256|ES256]      # generate signing keypair + JWKS
supython keygen rotate [--no-reload]          # add new verifying kid (zero-downtime)
supython keygen activate <kid> [--no-reload]  # promote kid to active signer
supython keygen prune [--force] [--no-reload] # drop retired kids past grace window
supython secret status                        # show symmetric secret manifest
supython secret rotate <storage|oauth>        # add new verifying symmetric secret
supython secret activate <storage|oauth> <kid>
supython secret prune <storage|oauth> [--force]
supython password rotate <role>               # rotate a Postgres role password

# Admin
supython admin create-user    # bootstrap the first admin (interactive)

# Realtime
supython realtime enable public.messages          # opt a table into realtime
supython realtime enable public.posts --owner-column author_id

# Jobs & worker
supython worker run --queue default               # start the job worker (blocks)
supython jobs list                                # list queued/running/finished jobs
supython jobs show <uuid>                         # show job details
supython jobs cancel <uuid>                       # cancel a job
supython jobs retry <uuid>                        # re-queue a failed job
supython jobs enqueue send_welcome_email --payload '{"email":"a@b.com"}'
supython cron list                                # list registered crons
supython cron sync                                # sync crons with pg_cron

# Test suite
supython test up                                  # start test Postgres on port 54323
supython test run [PYTEST_ARGS...]                # bootstrap + run full suite
supython test run tests/unit                      # fast loop, no Docker
supython test run tests/integration -k auth_signup
supython test down                                # stop test DB (keeps volume)
supython test reset                               # stop test DB + delete volume

How the auth ↔ PostgREST contract works

  1. supython signs an RS256 JWT with its private key and the claims:
    {
      "sub":   "<user uuid>",
      "email": "<user email>",
      "role":  "authenticated",
      "aud":   "authenticated",
      "iat":   ...,
      "exp":   ...
    }
    
  2. The client sends the token to either supython (for /auth/v1/user) or PostgREST (for everything else) as Authorization: Bearer <jwt>.
  3. PostgREST verifies the signature via the public JWKS, switches the DB role to authenticated, and sets request.jwt.claims so auth.uid() returns the user's id inside RLS policies.
  4. RLS policies on public.todos use auth.uid() to scope every query.

Docker image

The supython service ships as a multi-arch image (linux/amd64, linux/arm64) on python:3.11-slim. The admin UI bundle is committed into the wheel, so the image build is Node-free.

# build locally (host arch)
docker build -t supython:dev .

# build the multi-arch manifest
docker buildx build --platform linux/amd64,linux/arm64 -t supython:dev .

# run against an existing Postgres
docker run --rm -p 8000:8000 \
  -e DATABASE_URL=postgres://supython:supython@host.docker.internal:54322/supython \
  -e JWT_PRIVATE_KEY_PATH=/run/secrets/jwt.pem \
  -v ./.supython:/run/secrets:ro \
  supython:dev

The container runs as a non-root supython user (uid 1000), uses tini as PID 1, exposes port 8000 (override with SUPYTHON_PORT), and ships a /livez HEALTHCHECK. CI builds both arches on every PR via .github/workflows/docker.yml and publishes the multi-arch manifest to GHCR (ghcr.io/<owner>/supython:<tag>) on v* tags.

Running the test suite

The test suite is split in two so unit feedback is fast and integration runs are deterministic:

tests/unit/         # pure Python, no Docker (~6s) — run first, always
tests/integration/  # full ASGI + Postgres on port 54323

One-time setup:

supython test up    # start dedicated test Postgres on port 54323, apply migrations

Day-to-day:

supython test run                  # bootstrap + run full suite
supython test run tests/unit       # fast loop, no Docker required
supython test run tests/integration -k auth_signup
pytest tests/unit                  # unit-only without going through CLI

supython test run forwards every extra arg to pytest, sets DATABASE_URL to the test container, and exits with pytest's status code. Integration tests skip cleanly (not fail) when Postgres is unreachable, so unit tests always run in isolation.

CI: runners with Docker run supython test up && supython test run; runners without Docker run pytest tests/unit for a meaningful subset.

Roadmap [shipped v0.1.2]

  • ✅ Email/password auth, PostgREST contract, RLS demo
  • ✅ OAuth, password reset, magic link, OTP, reuse detection, email backend, test suite
  • ✅ Storage (S3/local) with RLS-on-metadata, edge-style functions from a functions/ directory; db.as_role(role, claims) helper; supython init scaffold; supython gen types --lang py
  • ✅ Realtime over LISTEN/NOTIFY with RLS-aware fan-out; Phoenix Channels wire format; broadcast + presence; examples/chat.html demo
  • ✅ Job queue worker + pg_cron scheduling + hooks + CLI management commands
  • ✅ Grooming + security foundation: unified versioning, CORS closed by default, RS256 JWT, rate limiting, supython doctor, pool sizing, statement timeout
  • ✅ Production observable: structured JSON logs, /livez//readyz//health, security headers, input size guards, audit log completeness, OAuth PKCE, secret rotation runbooks
  • ✅ (partial) Multi-arch Docker images, admin control plane (Vue 3 SPA — database, auth, storage, functions, realtime, jobs, backups, log tail), CI buildx workflow; benchmarks + security audit pass + dependency budget CI remaining
  • (deferred) — Realtime v2 over logical replication
  • v0.1.2 Release — final sweep, tag, publish wheel, production deployment with no patches
  • TypeScript SDK@supython/sdk wrapping @supabase/postgrest-js + @supabase/realtime-js

Post v0.1.2

  • v1.1+ — Admin control plane polish (backend + frontend shipped in v0.1.2; tests + remaining DoD items deferred)
  • Realtime v2 — logical replication (demand-driven; swap when trigger overhead or >8KB payload data warrants it)
  • Prometheus /metrics + OpenTelemetry — optional extras

License

MIT

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

supython-0.1.3.tar.gz (1.9 MB view details)

Uploaded Source

Built Distribution

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

supython-0.1.3-py3-none-any.whl (700.3 kB view details)

Uploaded Python 3

File details

Details for the file supython-0.1.3.tar.gz.

File metadata

  • Download URL: supython-0.1.3.tar.gz
  • Upload date:
  • Size: 1.9 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for supython-0.1.3.tar.gz
Algorithm Hash digest
SHA256 725b1aebcf391918d805abfc3534fe14a7aaeb449513b47b8bd4b725f960762f
MD5 72dc4cf85141f1d50f9e4c8f36359aab
BLAKE2b-256 67fa41c9241f22decefa16bfc5d6f73e00cf1e154eb0d0b863f48ab62427e2ac

See more details on using hashes here.

Provenance

The following attestation bundles were made for supython-0.1.3.tar.gz:

Publisher: pypi.yml on Tkeby/supython

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

File details

Details for the file supython-0.1.3-py3-none-any.whl.

File metadata

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

File hashes

Hashes for supython-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 fc6d936a36840024397aceeccd882df293077c1005e4b437788d823ec2b73e9f
MD5 d1caae77a59daba1bd4b94d3104cceb7
BLAKE2b-256 01578d514c7e4c67d9d19e8c3490898c2dbb7539a0065095a8e49ef319001ec1

See more details on using hashes here.

Provenance

The following attestation bundles were made for supython-0.1.3-py3-none-any.whl:

Publisher: pypi.yml on Tkeby/supython

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