supython 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. supython/__init__.py +24 -0
  2. supython/admin/__init__.py +3 -0
  3. supython/admin/api/__init__.py +24 -0
  4. supython/admin/api/auth.py +118 -0
  5. supython/admin/api/auth_templates.py +67 -0
  6. supython/admin/api/auth_users.py +225 -0
  7. supython/admin/api/db.py +174 -0
  8. supython/admin/api/functions.py +92 -0
  9. supython/admin/api/jobs.py +192 -0
  10. supython/admin/api/ops.py +224 -0
  11. supython/admin/api/realtime.py +281 -0
  12. supython/admin/api/service_auth.py +49 -0
  13. supython/admin/api/service_auth_templates.py +83 -0
  14. supython/admin/api/service_auth_users.py +346 -0
  15. supython/admin/api/service_db.py +214 -0
  16. supython/admin/api/service_functions.py +287 -0
  17. supython/admin/api/service_jobs.py +282 -0
  18. supython/admin/api/service_ops.py +213 -0
  19. supython/admin/api/service_realtime.py +30 -0
  20. supython/admin/api/service_storage.py +220 -0
  21. supython/admin/api/storage.py +117 -0
  22. supython/admin/api/system.py +37 -0
  23. supython/admin/audit.py +29 -0
  24. supython/admin/deps.py +22 -0
  25. supython/admin/errors.py +16 -0
  26. supython/admin/schemas.py +310 -0
  27. supython/admin/session.py +52 -0
  28. supython/admin/spa.py +38 -0
  29. supython/admin/static/assets/Alert-dluGVkos.js +49 -0
  30. supython/admin/static/assets/Audit-Njung3HI.js +2 -0
  31. supython/admin/static/assets/Backups-DzPlFgrm.js +2 -0
  32. supython/admin/static/assets/Buckets-ByacGkU1.js +2 -0
  33. supython/admin/static/assets/Channels-BoIuTtam.js +353 -0
  34. supython/admin/static/assets/ChevronRight-CtQH1EQ1.js +2 -0
  35. supython/admin/static/assets/CodeViewer-Bqy7-wvH.js +2 -0
  36. supython/admin/static/assets/Crons-B67vc39F.js +2 -0
  37. supython/admin/static/assets/DashboardView-CUTFVL6k.js +2 -0
  38. supython/admin/static/assets/DataTable-COAAWEft.js +747 -0
  39. supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js +75 -0
  40. supython/admin/static/assets/DrawerContent-TpYTFgF1.js +139 -0
  41. supython/admin/static/assets/Empty-cr2r7e2u.js +25 -0
  42. supython/admin/static/assets/EmptyState-DeDck-OL.js +2 -0
  43. supython/admin/static/assets/Grid-hFkp9F4P.js +2 -0
  44. supython/admin/static/assets/Input-DppYTq9C.js +259 -0
  45. supython/admin/static/assets/Invoke-DW3Nveeh.js +2 -0
  46. supython/admin/static/assets/JsonField-DibyJgun.js +2 -0
  47. supython/admin/static/assets/LoginView-BjLyE3Ds.css +1 -0
  48. supython/admin/static/assets/LoginView-CoOjECT_.js +111 -0
  49. supython/admin/static/assets/Logs-D9WYrnIT.js +2 -0
  50. supython/admin/static/assets/Logs-DS1XPa0h.css +1 -0
  51. supython/admin/static/assets/Migrations-DOSC2ddQ.js +2 -0
  52. supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js +2 -0
  53. supython/admin/static/assets/Queue-CywZs6vI.js +2 -0
  54. supython/admin/static/assets/RefreshTokens-Ccjr53jg.js +2 -0
  55. supython/admin/static/assets/RlsEditor-BSlH9vSc.js +2 -0
  56. supython/admin/static/assets/Routes-BiLXE49D.js +2 -0
  57. supython/admin/static/assets/Routes-C-ianIGD.css +1 -0
  58. supython/admin/static/assets/SchemaBrowser-DKy2_KQi.css +1 -0
  59. supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js +2 -0
  60. supython/admin/static/assets/Select-DIzZyRZb.js +434 -0
  61. supython/admin/static/assets/Space-n5-XcguU.js +400 -0
  62. supython/admin/static/assets/SqlEditor-b8pTsILY.js +3 -0
  63. supython/admin/static/assets/SqlWorkspace-BUS7IntH.js +104 -0
  64. supython/admin/static/assets/TableData-CQIagLKn.js +2 -0
  65. supython/admin/static/assets/Tag-D1fOKpTH.js +72 -0
  66. supython/admin/static/assets/Templates-BS-ugkdq.js +2 -0
  67. supython/admin/static/assets/Thing-CEAniuMg.js +107 -0
  68. supython/admin/static/assets/Users-wzwajhlh.js +2 -0
  69. supython/admin/static/assets/_plugin-vue_export-helper-DGA9ry_j.js +1 -0
  70. supython/admin/static/assets/dist-VXIJLCYq.js +13 -0
  71. supython/admin/static/assets/format-length-CGCY1rMh.js +2 -0
  72. supython/admin/static/assets/get-Ca6unauB.js +2 -0
  73. supython/admin/static/assets/index-CeE6v959.js +951 -0
  74. supython/admin/static/assets/pinia-COXwfrOX.js +2 -0
  75. supython/admin/static/assets/resources-Bt6thQCD.js +44 -0
  76. supython/admin/static/assets/use-locale-mtgM0a3a.js +2 -0
  77. supython/admin/static/assets/use-merged-state-BvhkaHNX.js +2 -0
  78. supython/admin/static/assets/useConfirm-tMjvBFXR.js +2 -0
  79. supython/admin/static/assets/useResource-C_rJCY8C.js +2 -0
  80. supython/admin/static/assets/useTable-CnZc5zhi.js +363 -0
  81. supython/admin/static/assets/useTable-Dg0XlRlq.css +1 -0
  82. supython/admin/static/assets/useToast-DsZKx0IX.js +2 -0
  83. supython/admin/static/assets/utils-sbXoq7Ir.js +2 -0
  84. supython/admin/static/favicon.svg +1 -0
  85. supython/admin/static/icons.svg +24 -0
  86. supython/admin/static/index.html +24 -0
  87. supython/app.py +162 -0
  88. supython/auth/__init__.py +3 -0
  89. supython/auth/_email_job.py +11 -0
  90. supython/auth/providers/__init__.py +34 -0
  91. supython/auth/providers/github.py +22 -0
  92. supython/auth/providers/google.py +19 -0
  93. supython/auth/providers/oauth.py +56 -0
  94. supython/auth/providers/registry.py +16 -0
  95. supython/auth/ratelimit.py +39 -0
  96. supython/auth/router.py +282 -0
  97. supython/auth/schemas.py +79 -0
  98. supython/auth/service.py +587 -0
  99. supython/backups/__init__.py +24 -0
  100. supython/backups/_backup_job.py +170 -0
  101. supython/backups/schemas.py +18 -0
  102. supython/backups/service.py +217 -0
  103. supython/body_size.py +184 -0
  104. supython/cli.py +1663 -0
  105. supython/client/__init__.py +67 -0
  106. supython/client/_auth.py +249 -0
  107. supython/client/_client.py +145 -0
  108. supython/client/_config.py +92 -0
  109. supython/client/_functions.py +69 -0
  110. supython/client/_storage.py +255 -0
  111. supython/client/py.typed +0 -0
  112. supython/db.py +151 -0
  113. supython/db_admin.py +8 -0
  114. supython/extensions.py +36 -0
  115. supython/functions/__init__.py +19 -0
  116. supython/functions/context.py +262 -0
  117. supython/functions/loader.py +307 -0
  118. supython/functions/router.py +228 -0
  119. supython/functions/schemas.py +50 -0
  120. supython/gen/__init__.py +5 -0
  121. supython/gen/_introspect.py +137 -0
  122. supython/gen/types_py.py +270 -0
  123. supython/gen/types_ts.py +365 -0
  124. supython/health.py +229 -0
  125. supython/hooks.py +117 -0
  126. supython/jobs/__init__.py +31 -0
  127. supython/jobs/backends.py +97 -0
  128. supython/jobs/context.py +58 -0
  129. supython/jobs/cron.py +152 -0
  130. supython/jobs/cron_inproc.py +119 -0
  131. supython/jobs/decorators.py +76 -0
  132. supython/jobs/registry.py +79 -0
  133. supython/jobs/router.py +136 -0
  134. supython/jobs/schemas.py +92 -0
  135. supython/jobs/service.py +311 -0
  136. supython/jobs/worker.py +219 -0
  137. supython/jwks.py +257 -0
  138. supython/keyset.py +279 -0
  139. supython/logging_config.py +291 -0
  140. supython/mail.py +33 -0
  141. supython/mailer.py +65 -0
  142. supython/migrate.py +81 -0
  143. supython/migrations/0001_extensions_and_roles.sql +46 -0
  144. supython/migrations/0002_auth_schema.sql +66 -0
  145. supython/migrations/0003_demo_todos.sql +42 -0
  146. supython/migrations/0004_auth_v0_2.sql +47 -0
  147. supython/migrations/0005_storage_schema.sql +117 -0
  148. supython/migrations/0006_realtime_schema.sql +206 -0
  149. supython/migrations/0007_jobs_schema.sql +254 -0
  150. supython/migrations/0008_jobs_last_error.sql +56 -0
  151. supython/migrations/0009_auth_rate_limits.sql +33 -0
  152. supython/migrations/0010_worker_heartbeat.sql +14 -0
  153. supython/migrations/0011_admin_schema.sql +45 -0
  154. supython/migrations/0012_auth_banned_until.sql +10 -0
  155. supython/migrations/0013_email_templates.sql +19 -0
  156. supython/migrations/0014_realtime_payload_warning.sql +96 -0
  157. supython/migrations/0015_backups_schema.sql +14 -0
  158. supython/passwords.py +15 -0
  159. supython/realtime/__init__.py +6 -0
  160. supython/realtime/broker.py +814 -0
  161. supython/realtime/protocol.py +234 -0
  162. supython/realtime/router.py +184 -0
  163. supython/realtime/schemas.py +207 -0
  164. supython/realtime/service.py +261 -0
  165. supython/realtime/topics.py +175 -0
  166. supython/realtime/websocket.py +586 -0
  167. supython/scaffold/__init__.py +5 -0
  168. supython/scaffold/init_project.py +144 -0
  169. supython/scaffold/templates/Caddyfile.tmpl +4 -0
  170. supython/scaffold/templates/README.md.tmpl +22 -0
  171. supython/scaffold/templates/apps_hooks.py.tmpl +11 -0
  172. supython/scaffold/templates/apps_jobs.py.tmpl +8 -0
  173. supython/scaffold/templates/asgi.py.tmpl +14 -0
  174. supython/scaffold/templates/docker-compose.prod.yml.tmpl +84 -0
  175. supython/scaffold/templates/docker-compose.yml.tmpl +45 -0
  176. supython/scaffold/templates/docker_postgres_Dockerfile.tmpl +9 -0
  177. supython/scaffold/templates/docker_postgres_postgresql.conf.tmpl +3 -0
  178. supython/scaffold/templates/env.example.tmpl +168 -0
  179. supython/scaffold/templates/functions_README.md.tmpl +21 -0
  180. supython/scaffold/templates/gitignore.tmpl +14 -0
  181. supython/scaffold/templates/manage.py.tmpl +11 -0
  182. supython/scaffold/templates/migrations/.gitkeep +0 -0
  183. supython/scaffold/templates/package_init.py.tmpl +1 -0
  184. supython/scaffold/templates/settings.py.tmpl +31 -0
  185. supython/secretset.py +347 -0
  186. supython/security_headers.py +78 -0
  187. supython/settings.py +244 -0
  188. supython/settings_module.py +117 -0
  189. supython/storage/__init__.py +5 -0
  190. supython/storage/backends.py +392 -0
  191. supython/storage/router.py +341 -0
  192. supython/storage/schemas.py +50 -0
  193. supython/storage/service.py +445 -0
  194. supython/storage/signing.py +119 -0
  195. supython/tokens.py +85 -0
  196. supython-0.1.0.dist-info/METADATA +756 -0
  197. supython-0.1.0.dist-info/RECORD +200 -0
  198. supython-0.1.0.dist-info/WHEEL +4 -0
  199. supython-0.1.0.dist-info/entry_points.txt +2 -0
  200. supython-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,756 @@
1
+ Metadata-Version: 2.4
2
+ Name: supython
3
+ Version: 0.1.0
4
+ Summary: A lightweight Postgres-first BaaS framework for Python
5
+ Project-URL: Homepage, https://github.com/Tkeby/supython
6
+ Project-URL: Repository, https://github.com/Tkeby/supython
7
+ Project-URL: Issues, https://github.com/Tkeby/supython/issues
8
+ Project-URL: Changelog, https://github.com/Tkeby/supython/blob/main/CHANGELOG.md
9
+ Project-URL: Documentation, https://github.com/Tkeby/supython/blob/main/docs/PROJECT.md
10
+ Author-email: Tkeby <tsegaw.dev@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: auth,baas,fastapi,postgres,postgrest
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Framework :: FastAPI
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Database
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.11
27
+ Requires-Dist: aiosmtplib>=3.0
28
+ Requires-Dist: argon2-cffi>=23.1
29
+ Requires-Dist: asyncpg>=0.30
30
+ Requires-Dist: authlib>=1.3
31
+ Requires-Dist: email-validator>=2.2
32
+ Requires-Dist: fastapi>=0.115
33
+ Requires-Dist: httpx>=0.28
34
+ Requires-Dist: itsdangerous>=2.2
35
+ Requires-Dist: pydantic-settings>=2.6
36
+ Requires-Dist: pydantic>=2.9
37
+ Requires-Dist: pyjwt[crypto]>=2.10
38
+ Requires-Dist: python-multipart>=0.0.20
39
+ Requires-Dist: typer>=0.15
40
+ Requires-Dist: uvicorn[standard]>=0.32
41
+ Provides-Extra: arq
42
+ Requires-Dist: arq>=0.26; extra == 'arq'
43
+ Provides-Extra: client
44
+ Requires-Dist: httpx>=0.28; extra == 'client'
45
+ Requires-Dist: postgrest-py>=0.16.0; extra == 'client'
46
+ Requires-Dist: realtime>=2.0.0; extra == 'client'
47
+ Provides-Extra: cron-inproc
48
+ Requires-Dist: croniter>=2.0; extra == 'cron-inproc'
49
+ Provides-Extra: dev
50
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
51
+ Requires-Dist: pytest>=8.3; extra == 'dev'
52
+ Requires-Dist: ruff>=0.8; extra == 'dev'
53
+ Provides-Extra: dramatiq
54
+ Requires-Dist: dramatiq>=1.17; extra == 'dramatiq'
55
+ Provides-Extra: s3
56
+ Requires-Dist: aioboto3>=13.0; extra == 's3'
57
+ Description-Content-Type: text/markdown
58
+
59
+ # supython
60
+
61
+ > A lightweight, Postgres-first BaaS framework for Python. **v0.1.0 release**
62
+
63
+ supython is the inverse of Django: **the database owns the schema, Python owns
64
+ the things SQL is bad at**. It leans on [PostgREST](https://postgrest.org)
65
+ for auto-generated REST APIs and on Postgres' own RLS for authorization,
66
+ while a small FastAPI service in Python handles auth, JWT issuance, realtime,
67
+ storage, functions, workers, and an optional admin control plane.
68
+
69
+ supython is for a specific person with a specific problem:
70
+ > 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.
71
+
72
+ Shipped [v0.1.0]:
73
+
74
+ **Core platform**
75
+ - **Email/password auth** — signup, login, refresh-token rotation with **reuse detection**
76
+ - **OAuth** (Google + GitHub) via `authlib` with PKCE
77
+ - **Password reset**, **magic link**, and **email OTP** (pluggable email backend)
78
+ - **RS256 JWT** — asymmetric signing; PostgREST verifies via shared JWKS; zero-downtime key rotation
79
+ - **Rate limiting** on auth endpoints (per-IP fixed-window counters)
80
+ - **Row-Level Security** — `auth.uid()` helper, `request.jwt.claims` GUC, role-scoped DB access via `db.as_role()`
81
+ - **`supython init`** — scaffold a new project in one command
82
+ - **`supython gen types --lang py`** — emit typed dataclasses + TypedDicts from your Postgres schema
83
+ - **zero Python CRUD code** — every read, write, filter, sort, and pagination is served by PostgREST under RLS
84
+
85
+ **Storage & functions**
86
+ - **S3/local storage** with RLS-on-metadata, signed URLs, multipart upload, and range download
87
+ - **Edge functions** from a `functions/` directory with hot reload and role-scoped DB access
88
+
89
+ **Realtime**
90
+ - **WebSocket Realtime** — `postgres_changes`, `broadcast`, `presence` with per-subscriber RLS filtering
91
+ - **Phoenix Channels wire format** — unmodified `supabase-js` / `supabase-py` SDKs connect
92
+ - **Generic trigger** — `realtime.enable('public.todos')` opts any table in
93
+ - **Two-browser chat demo** — `examples/chat.html` (zero build step)
94
+
95
+ **Jobs & cron**
96
+ - **Job queue** — Postgres-backed (`SELECT FOR UPDATE SKIP LOCKED`), idempotent enqueue, retry with backoff
97
+ - **Cron scheduling** — `pg_cron` (primary) or in-process `croniter` fallback
98
+ - **Generic hooks** — `@app.on_signup` / `@app.on_login` lifecycle hooks
99
+ - **`supython worker run`** — long-running worker with graceful SIGTERM drain
100
+
101
+ **Operations & security**
102
+ - **Structured JSON logs** — request-id propagation, redacted auth headers, tracebacks on 5xx
103
+ - **`/livez` / `/readyz` / `/health`** — liveness, dependency readiness (DB, PostgREST, broker, worker), full detail
104
+ - **Security headers** — HSTS, CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy
105
+ - **`supython doctor`** — diagnoses roles, extensions, grants, JWKS, migration drift, symmetric secrets
106
+ - **Secret rotation** — JWT keys, symmetric secrets, Postgres passwords; all with zero-downtime runbooks
107
+ - **Multi-arch Docker image** — `linux/amd64` + `linux/arm64`, non-root user, `tini` PID 1, ~64 MB
108
+
109
+ **Admin control plane** (shipped in v0.1.0)
110
+ - **Vue 3 + Vite SPA** at `/admin` — no runtime Node deps; pre-built static bundle in the wheel
111
+ - **Database surface** — schema browser, table data with role switcher, SQL workspace (read-only default + write toggle), RLS policy editor with dry-run, migrations panel
112
+ - **Auth surface** — user search, ban/unban/force-logout, refresh-token inspector, audit log, email template editing
113
+ - **Module screens** — storage buckets/objects, function routes/invoke, realtime tables/channels, job queue/crons
114
+ - **Ops** — backup management, live log tail via SSE with level/request-id filters
115
+ - **`supython admin create-user`** — bootstrap the first admin (no chicken-and-egg)
116
+
117
+ ## Architecture
118
+
119
+ ```
120
+ ┌──────────────────────┐
121
+ client ─────► │ supython (FastAPI) │ /auth/v1/*, /storage/v1/*,
122
+ │ port 8000 │ /functions/v1/*, /jobs/v1/*,
123
+ │ │ /admin, /livez, /readyz ──┐
124
+ └──────────────────────┘ │
125
+ │ │
126
+ ┌──────────────────────┐ │
127
+ client ─────► │ PostgREST │ /<table> ─┤
128
+ │ port 54321 │ │
129
+ └──────────────────────┘ │
130
+ │ │
131
+ ▼ │
132
+ ┌──────────────────────┐ ◄───────────────────────────┘
133
+ │ Postgres │
134
+ │ port 54322 │
135
+ │ roles: anon / │
136
+ │ authenticated / │
137
+ │ service_role │
138
+ └──────────────────────┘
139
+ ```
140
+
141
+ The unifying contract is the **JWT** + **Postgres role system**. supython
142
+ mints the JWT; PostgREST verifies the signature via shared JWKS and runs
143
+ every request under the role + claims it carries. RLS does the rest.
144
+
145
+ ## Quick start
146
+
147
+ Requires Python 3.11+, Docker 24+ with the `compose` plugin.
148
+
149
+ ```bash
150
+ # 1. install the wheel
151
+ python -m venv .venv && source .venv/bin/activate
152
+ pip install supython
153
+
154
+ # 2. scaffold a new project
155
+ supython init myapp
156
+ cd myapp
157
+ cp .env.example .env
158
+ # Review .env — at minimum confirm AUTHENTICATOR_PASSWORD matches
159
+ # the value PostgREST will use (docker-compose.yml injects it via env).
160
+ #
161
+ # The scaffold creates:
162
+ # manage.py — Django-style CLI entrypoint (sets SUPYTHON_SETTINGS_MODULE)
163
+ # myapp/settings.py — declare EXTENSIONS, EXTRA_ROUTERS, EXTRA_MIDDLEWARE
164
+ # myapp/jobs.py — example @job seed (register your background jobs here)
165
+ # myapp/hooks.py — example @on("signup") seed (lifecycle hooks)
166
+ # myapp/asgi.py — optional entrypoint for uvicorn/gunicorn
167
+
168
+ # 3. boot Postgres + PostgREST and run migrations (one command)
169
+ supython up
170
+
171
+ # 4. run the auth/API service (separate terminal)
172
+ supython dev
173
+
174
+ # 5. (optional) generate typed Python classes from your Postgres schema
175
+ supython gen types --lang py --out types.py
176
+
177
+ # 6. (optional) bootstrap the admin dashboard
178
+ supython admin create-user
179
+ # then open http://localhost:8000/admin
180
+ ```
181
+
182
+ You should now have:
183
+
184
+ | service | url |
185
+ | ---------- | ---------------------------- |
186
+ | supython | http://localhost:8000 |
187
+ | Admin UI | http://localhost:8000/admin |
188
+ | PostgREST | http://localhost:54321 |
189
+ | Postgres | `postgres://supython:supython@localhost:54322/supython` |
190
+
191
+ ## End-to-end smoke test
192
+
193
+ ```bash
194
+ # sign up
195
+ curl -sS -X POST http://localhost:8000/auth/v1/signup \
196
+ -H 'content-type: application/json' \
197
+ -d '{"email":"alice@example.com","password":"password123"}'
198
+
199
+ # get a fresh token
200
+ TOKEN=$(curl -sS -X POST http://localhost:8000/auth/v1/token \
201
+ -H 'content-type: application/json' \
202
+ -d '{"email":"alice@example.com","password":"password123"}' \
203
+ | python -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
204
+
205
+ # create a todo via PostgREST — note: NO Python code involved
206
+ curl -sS -X POST http://localhost:54321/todos \
207
+ -H "authorization: Bearer $TOKEN" \
208
+ -H 'content-type: application/json' \
209
+ -H 'prefer: return=representation' \
210
+ -d '{"title":"buy milk"}'
211
+
212
+ # list todos — RLS hides everyone else's rows
213
+ curl -sS http://localhost:54321/todos -H "authorization: Bearer $TOKEN"
214
+ ```
215
+
216
+ See [`examples/todos.http`](examples/todos.http) for the full set of calls
217
+ including filtering, sorting, refresh, and isolation between users.
218
+
219
+ ## Realtime quickstart
220
+
221
+ supython ships a WebSocket engine that speaks the **Phoenix Channels 5-tuple
222
+ protocol** used by every official Supabase SDK. Unmodified `supabase-js` and
223
+ `supabase-py` clients connect without any shim.
224
+
225
+ ### 1. Opt a table into realtime
226
+
227
+ ```sql
228
+ -- run once (or add to a migration)
229
+ SELECT realtime.enable('public.messages');
230
+ -- with a custom owner column for DELETE visibility:
231
+ -- SELECT realtime.enable('public.messages', 'author_id');
232
+ ```
233
+
234
+ Or via CLI (requires a running server):
235
+
236
+ ```bash
237
+ supython realtime enable public.messages
238
+ ```
239
+
240
+ ### 2. Open the demo
241
+
242
+ ```bash
243
+ # start the stack
244
+ supython up
245
+ supython dev # in a second terminal
246
+
247
+ # open the chat demo in two browser tabs
248
+ python -m http.server --directory examples 8080
249
+ # → http://localhost:8080/chat.html
250
+ ```
251
+
252
+ Enter your JWT (or leave blank for `anon` role), pick a room name, and open
253
+ the same page in a second tab. Messages broadcast instantly; Postgres row
254
+ changes appear as structured cards.
255
+
256
+ ### 3. Subscribe from JavaScript (no SDK required)
257
+
258
+ ```js
259
+ // Phoenix Channels 5-tuple: [join_ref, ref, topic, event, payload]
260
+ const ws = new WebSocket(
261
+ "ws://localhost:8000/realtime/v1/websocket?apikey=<JWT>&vsn=1.0.0"
262
+ );
263
+
264
+ let ref = 0;
265
+ ws.onopen = () => {
266
+ ws.send(JSON.stringify(["1", String(++ref), "realtime:room-42", "phx_join", {
267
+ config: {
268
+ postgres_changes: [{ event: "*", schema: "public", table: "messages" }],
269
+ broadcast: { self: false }
270
+ },
271
+ access_token: "<JWT>"
272
+ }]));
273
+ };
274
+
275
+ ws.onmessage = ({ data }) => {
276
+ const [, , topic, event, payload] = JSON.parse(data);
277
+ if (event === "postgres_changes") console.log(payload.data);
278
+ if (event === "broadcast") console.log(payload.payload);
279
+ };
280
+ ```
281
+
282
+ ### 4. Subscribe from Python
283
+
284
+ ```python
285
+ import asyncio, json
286
+ import websockets
287
+
288
+ TOKEN = "eyJ..." # or omit for anon
289
+
290
+ async def main():
291
+ url = f"ws://localhost:8000/realtime/v1/websocket?apikey={TOKEN}&vsn=1.0.0"
292
+ async with websockets.connect(url) as ws:
293
+ await ws.send(json.dumps([
294
+ "1", "1", "realtime:room-42", "phx_join",
295
+ {"config": {"broadcast": {"self": True}}, "access_token": TOKEN}
296
+ ]))
297
+ async for raw in ws:
298
+ join_ref, ref, topic, event, payload = json.loads(raw)
299
+ print(event, payload)
300
+
301
+ asyncio.run(main())
302
+ ```
303
+
304
+ ### 5. REST broadcast (server → clients)
305
+
306
+ ```bash
307
+ curl -sS -X POST http://localhost:8000/realtime/v1/broadcast/room-42 \
308
+ -H "authorization: Bearer $SERVICE_ROLE_TOKEN" \
309
+ -H "content-type: application/json" \
310
+ -d '{"event": "announcement", "payload": {"text": "Hello from the server!"}}'
311
+ ```
312
+
313
+ ### Realtime settings (`.env`)
314
+
315
+ | Variable | Default | Purpose |
316
+ |---|---|---|
317
+ | `REALTIME_ENABLED` | `true` | Toggle the realtime module |
318
+ | `REALTIME_NOTIFY_CHANNEL` | `realtime:changes` | Postgres `LISTEN` channel |
319
+ | `REALTIME_MAX_CONNECTIONS` | `1000` | Max concurrent WS clients |
320
+ | `REALTIME_MAX_SUBS_PER_CONN` | `100` | Max channel joins per connection |
321
+ | `REALTIME_HEARTBEAT_TIMEOUT_SECONDS` | `30` | Idle-close timeout (client sends every 25s) |
322
+ | `REALTIME_BROKER_QUEUE_SIZE` | `1000` | Per-subscriber outbound queue depth |
323
+
324
+ ## Jobs & cron quickstart
325
+
326
+ ### 1. Define a job
327
+
328
+ ```python
329
+ # in your application code
330
+ from supython.jobs.decorators import job
331
+
332
+ @job("send_welcome_email", version=1, max_attempts=5)
333
+ async def send_welcome_email(ctx, payload):
334
+ await ctx.send_email(
335
+ to=payload["email"],
336
+ subject="Welcome!",
337
+ text="Thanks for signing up.",
338
+ )
339
+ ```
340
+
341
+ ### 2. Enqueue from a function or hook
342
+
343
+ ```python
344
+ from supython import hooks
345
+ from supython.jobs.service import enqueue
346
+
347
+ @hooks.on("signup")
348
+ async def on_signup(user, ctx):
349
+ await enqueue(
350
+ ctx.db,
351
+ name="send_welcome_email",
352
+ payload={"email": user.email},
353
+ idempotency_key=f"welcome:{user.id}",
354
+ )
355
+ ```
356
+
357
+ ### 3. Schedule a cron
358
+
359
+ ```python
360
+ from supython.jobs.decorators import cron
361
+
362
+ @cron("*/5 * * * *", name="cleanup", job_name="cleanup_job")
363
+ async def cleanup_job(ctx, payload):
364
+ ctx.logger.info("running periodic cleanup")
365
+ ```
366
+
367
+ ### 4. Run the worker
368
+
369
+ ```bash
370
+ supython worker run --queue default --concurrency 5
371
+ ```
372
+
373
+ Or enable in-process mode for development:
374
+
375
+ ```bash
376
+ # in .env
377
+ JOBS_DEV_INPROCESS=true
378
+ supython dev
379
+ ```
380
+
381
+ ### Jobs settings (`.env`)
382
+
383
+ | Variable | Default | Purpose |
384
+ |---|---|---|
385
+ | `JOBS_ENABLED` | `true` | Toggle the jobs module |
386
+ | `JOBS_BACKEND` | `pg` | Queue backend (only `pg` for now) |
387
+ | `JOBS_CRON_BACKEND` | `pg_cron` | `pg_cron`, `inproc`, or `off` |
388
+ | `JOBS_POLL_INTERVAL_S` | `1.0` | Seconds between queue polls |
389
+ | `JOBS_CONCURRENCY` | `5` | Max concurrent jobs per worker |
390
+ | `JOBS_DEFAULT_MAX_ATTEMPTS` | `3` | Default retry limit |
391
+ | `JOBS_BACKOFF_BASE_S` | `5.0` | Base backoff delay (seconds) |
392
+ | `JOBS_BACKOFF_MAX_S` | `300.0` | Max backoff delay (seconds) |
393
+ | `JOBS_VISIBILITY_TIMEOUT_S` | `300.0` | Zombie reclaim timeout (seconds) |
394
+ | `JOBS_DRAIN_TIMEOUT_S` | `30.0` | Graceful shutdown drain (seconds) |
395
+ | `JOBS_DEV_INPROCESS` | `false` | Spawn worker in-process during `supython dev` |
396
+
397
+ ## v0.2 auth endpoints
398
+
399
+ | Endpoint | Method | Purpose |
400
+ |---|---|---|
401
+ | `/auth/v1/signup` | POST | Create account, return token pair |
402
+ | `/auth/v1/token` | POST | Password login |
403
+ | `/auth/v1/refresh` | POST | Rotate refresh token (reuse detection built in) |
404
+ | `/auth/v1/logout` | POST | Revoke refresh token |
405
+ | `/auth/v1/user` | GET | Return the caller's user (JWT required) |
406
+ | `/auth/v1/recover` | POST | Request password-reset email |
407
+ | `/auth/v1/recover/verify` | POST | Verify reset token, set new password |
408
+ | `/auth/v1/magiclink` | POST | Request magic-link email |
409
+ | `/auth/v1/magiclink/verify` | GET | Verify magic-link token (`?token=…`) |
410
+ | `/auth/v1/otp` | POST | Request email OTP |
411
+ | `/auth/v1/otp/verify` | POST | Verify OTP code |
412
+ | `/auth/v1/authorize/{provider}` | GET | Start OAuth flow (redirect to provider) |
413
+ | `/auth/v1/callback/{provider}` | GET | Handle OAuth callback, redirect with tokens |
414
+
415
+ **Email backend** — set `EMAIL_BACKEND=console` (default, logs to stdout) or
416
+ `EMAIL_BACKEND=smtp` and configure `SMTP_HOST / SMTP_PORT / SMTP_USERNAME /
417
+ SMTP_PASSWORD`.
418
+
419
+ **OAuth** — add `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` (and/or GitHub
420
+ equivalents) to `.env`. Providers without credentials are silently disabled.
421
+
422
+ ### Auth hardening settings (`.env`)
423
+
424
+ | Variable | Default | Purpose |
425
+ |---|---|---|
426
+ | `DB_STATEMENT_TIMEOUT_MS` | `30000` | Per-connection query timeout for the asyncpg pool (`0` disables) |
427
+ | `DB_POOL_MIN_SIZE` | `1` | Minimum asyncpg pool size |
428
+ | `DB_POOL_MAX_SIZE` | `10` | Maximum asyncpg pool size |
429
+ | `AUTH_RATE_LIMIT_ENABLED` | `true` | Toggle auth endpoint rate limiting |
430
+ | `AUTH_RATE_LIMIT_WINDOW_SECONDS` | `60` | Fixed-window size for auth endpoint counters |
431
+ | `AUTH_RATE_LIMIT_TOKEN_PER_WINDOW` | `10` | `/auth/v1/token` attempts per IP/window |
432
+ | `AUTH_RATE_LIMIT_SIGNUP_PER_WINDOW` | `5` | `/auth/v1/signup` attempts per IP/window |
433
+ | `AUTH_RATE_LIMIT_RECOVER_PER_WINDOW` | `3` | `/auth/v1/recover` attempts per IP/window |
434
+ | `AUTH_RATE_LIMIT_OTP_PER_WINDOW` | `5` | `/auth/v1/otp` attempts per IP/window |
435
+ | `AUTH_RATE_LIMIT_MAGICLINK_PER_WINDOW` | `5` | `/auth/v1/magiclink` attempts per IP/window |
436
+
437
+ **`AUTHENTICATOR_PASSWORD`** — the password used for the `authenticator` Postgres
438
+ role that PostgREST connects as. Defaults to `authenticator` (matches the
439
+ migration), but you should change it in production. `supython up` automatically
440
+ runs `ALTER ROLE authenticator WITH PASSWORD …` after migrations so you never
441
+ need to edit SQL.
442
+
443
+ ## What's in the box
444
+
445
+ ```
446
+ supython/
447
+ ├── manage.py # Django-style entrypoint (sets SUPYTHON_SETTINGS_MODULE)
448
+ ├── docker-compose.yml # Postgres + PostgREST (dev stack)
449
+ ├── docker-compose.prod.yml # hardened single-host production stack
450
+ ├── docker-compose.test.yml # dedicated test Postgres on port 54323
451
+ ├── Caddyfile # reverse-proxy TLS for prod
452
+ ├── <name>/ # your Python package
453
+ │ ├── __init__.py
454
+ │ ├── settings.py # project settings (EXTENSIONS, EXTRA_ROUTERS, …)
455
+ │ ├── asgi.py # optional ASGI entrypoint (uvicorn <name>.asgi:app)
456
+ │ ├── jobs.py # example @job seed
457
+ │ └── hooks.py # example @on("signup") seed
458
+ ├── migrations/
459
+ │ ├── 0001_extensions_and_roles.sql # anon/authenticated/service_role/authenticator
460
+ │ ├── 0002_auth_schema.sql # auth.users, auth.refresh_tokens, auth.uid()
461
+ │ ├── 0003_demo_todos.sql # the demo table + RLS policies
462
+ │ ├── 0004_auth_v0_2.sql # identities, one_time_tokens, audit_log
463
+ │ ├── 0005_storage_schema.sql # storage.buckets + storage.objects
464
+ │ ├── 0006_realtime_schema.sql # realtime schema, trigger, enable() helper
465
+ │ ├── 0007_jobs_schema.sql # jobs queue, cron_schedules, enqueue/claim_next
466
+ │ ├── 0008_jobs_last_error.sql # last_error column on jobs.jobs
467
+ │ ├── 0009_auth_rate_limits.sql # auth fixed-window rate-limit counters
468
+ │ ├── 0010_worker_heartbeat.sql # worker heartbeat for /readyz
469
+ │ ├── 0011_admin_schema.sql # admin.admin_users, sessions, audit
470
+ │ ├── 0012_auth_banned_until.sql # banned_until on auth.users
471
+ │ ├── 0013_email_templates.sql # auth.email_templates
472
+ │ ├── 0014_realtime_payload_warning.sql # >8KB payload warning counter
473
+ │ └── 0015_backups_schema.sql # backups metadata
474
+ ├── examples/
475
+ │ ├── todos.http # HTTP smoke tests (auth + PostgREST)
476
+ │ ├── storage.http # storage upload / signed URL examples
477
+ │ ├── functions.http # edge-function call examples
478
+ │ └── chat.html # two-browser realtime demo (vanilla JS, zero deps)
479
+ ├── docs/
480
+ │ ├── PROJECT.md # architecture + roadmap (single source of truth)
481
+ │ ├── Installation.md # full install guide (dev, prod, managed Postgres)
482
+ │ └── admin-ui/
483
+ │ ├── admin-surface-plan.md # admin implementation plan + phase status
484
+ │ └── admin-surface.md # admin architecture + contracts
485
+ ├── tests/
486
+ │ ├── conftest.py # cross-tree fixtures (keys, capturing mailer)
487
+ │ ├── _keys.py # JWT-forging helpers
488
+ │ ├── fixtures/ # test function modules, etc.
489
+ │ ├── unit/ # pure-Python tests (~6s, no Docker)
490
+ │ │ ├── test_admin_session.py
491
+ │ │ ├── test_admin_service_db.py
492
+ │ │ └── ...
493
+ │ └── integration/ # full ASGI + Postgres on port 54323
494
+ │ ├── conftest.py # pool, app, client, autouse DB cleaners
495
+ │ ├── test_auth_signup_login.py
496
+ │ ├── test_admin_auth.py
497
+ │ ├── test_admin_auth_users.py
498
+ │ ├── test_admin_db_rows.py
499
+ │ ├── test_admin_jobs.py
500
+ │ ├── test_admin_ops_backups.py
501
+ │ ├── test_admin_storage.py
502
+ │ ├── test_postgrest_rls.py
503
+ │ ├── test_realtime_ws.py
504
+ │ └── ...
505
+ ├── admin-ui/ # Vue 3 + Vite SPA (built → src/supython/admin/static/)
506
+ │ └── src/
507
+ │ ├── api/ # single fetch seam
508
+ │ ├── components/ # shell, data, editors, feedback
509
+ │ ├── composables/ # useResource, useTable, useConfirm, useImpersonate, …
510
+ │ ├── stores/ # auth, ui
511
+ │ ├── views/ # Dashboard, db/, auth/, storage/, functions/, …
512
+ │ └── router/
513
+ └── src/supython/
514
+ ├── __init__.py # single version string
515
+ ├── settings.py # pydantic-settings, .env-driven
516
+ ├── db.py # asyncpg pool + lifespan + as_role() / as_service_role()
517
+ ├── mailer.py # ConsoleBackend / SmtpBackend
518
+ ├── tokens.py # RS256/ES256 JWT + JWKS
519
+ ├── passwords.py # argon2id
520
+ ├── migrate.py # ~50-line SQL migration runner
521
+ ├── app.py # FastAPI factory
522
+ ├── cli.py # typer: up, dev, keygen, admin, worker, test, …
523
+ ├── extensions.py # eager-import dotted module paths at boot
524
+ ├── settings_module.py # Django-style user settings (EXTENSIONS, EXTRA_ROUTERS, …)
525
+ ├── health.py # /livez, /readyz, /health endpoints
526
+ ├── logging_config.py # structured JSON log setup
527
+ ├── security_headers.py # HSTS, CSP, etc.
528
+ ├── body_size.py # request body size guards
529
+ ├── jwks.py # JWKS generation + rotation helpers
530
+ ├── keyset.py # asymmetric key rotation manifest
531
+ ├── secretset.py # symmetric secret rotation manifest
532
+ ├── hooks.py # generic hook system: on() / fire()
533
+ ├── mail.py # email send with job-retry fallback
534
+ ├── auth/
535
+ │ ├── schemas.py
536
+ │ ├── service.py # full auth layer: signup / OAuth / OTP / recover …
537
+ │ ├── router.py # all /auth/v1/* routes
538
+ │ └── providers/ # Google, GitHub, OAuth2 helpers
539
+ ├── storage/
540
+ │ ├── backends.py # LocalBackend, S3Backend
541
+ │ ├── service.py
542
+ │ └── router.py # /storage/v1/*
543
+ ├── functions/
544
+ │ ├── loader.py # filesystem discovery + hot reload
545
+ │ └── router.py # /functions/v1/*
546
+ ├── realtime/
547
+ │ ├── protocol.py # Phoenix Channels encode/decode
548
+ │ ├── broker.py # fan-out engine with RLS filtering
549
+ │ ├── websocket.py # WS route with JWT auth
550
+ │ └── router.py # /realtime/v1/*
551
+ ├── jobs/
552
+ │ ├── registry.py # @job / @cron decorator store
553
+ │ ├── service.py # enqueue, claim_next, mark_*
554
+ │ ├── worker.py # long-running poll/dispatch/drain loop
555
+ │ ├── cron.py # pg_cron sync + InProcScheduler
556
+ │ └── router.py # /jobs/v1/*
557
+ ├── admin/
558
+ │ ├── session.py # admin cookie session (SHA-256 hashed, 8h TTL)
559
+ │ ├── deps.py # require_admin dependency
560
+ │ ├── spa.py # static SPA mount + index.html fallback
561
+ │ ├── schemas.py # shared Pydantic models
562
+ │ ├── audit.py # admin audit log writer
563
+ │ ├── static/ # pre-built Vue 3 SPA bundle (committed)
564
+ │ └── api/ # /admin/api/v1/* route handlers
565
+ ├── backups/ # pg_dump wrapper + restore
566
+ ├── gen/ # supython gen types --lang py|ts
567
+ ├── scaffold/ # supython init templates
568
+ └── client/ # Python SDK (optional [client] extra)
569
+ ```
570
+
571
+ ## Plugins & extensions
572
+
573
+ supython uses a Django-style settings module to declare your app's extensions:
574
+
575
+ ```python
576
+ # <name>/settings.py — scaffolded by `supython init`
577
+
578
+ EXTENSIONS = [
579
+ "myapp.jobs", # your @job / @cron decorators
580
+ "myapp.hooks", # your @on("signup") / @on("login") hooks
581
+ ]
582
+
583
+ EXTRA_ROUTERS: list[str] = [] # "module.path:router_symbol"
584
+ EXTRA_MIDDLEWARE: list[str] = [] # "module.path:ClassName"
585
+ ```
586
+
587
+ The scaffolded `manage.py` sets `SUPYTHON_SETTINGS_MODULE` so the CLI and
588
+ worker automatically discover your extensions. You can also set
589
+ `EXTENSIONS=myapp.jobs,myapp.hooks` directly in `.env` if you prefer not
590
+ to use a settings module.
591
+
592
+ Extensions are plain Python modules imported eagerly at boot — their
593
+ `@job`, `@cron`, and `@on` decorators register before the FastAPI app or
594
+ worker starts. This is the same mechanism supython's own internals use
595
+ to stay composable without cross-module imports.
596
+
597
+ ## CLI
598
+
599
+ ```
600
+ supython up # docker compose up + migrate + start postgrest
601
+ supython up --prod # boot the production stack
602
+ supython up --prod --worker # boot prod stack + worker
603
+ supython dev # uvicorn the FastAPI service with reload
604
+ supython down # stop the stack (keeps data)
605
+ supython down --prod # stop the prod stack
606
+ supython reset # stop the stack and DELETE the volume (destructive)
607
+ supython reset --prod # stop prod stack and DELETE volumes
608
+ supython migrate # apply pending SQL migrations
609
+ supython info # print resolved settings
610
+ supython doctor # diagnose roles, extensions, JWKS, grants, migration drift
611
+ supython init <name> # scaffold a new supython project
612
+ supython gen types --lang py --out types.py # emit typed dataclasses + TypedDicts
613
+ supython gen types --lang ts --out types.ts # emit TypeScript Database interface
614
+
615
+ # Auth & key management
616
+ supython keygen init [--alg RS256|ES256] # generate signing keypair + JWKS
617
+ supython keygen rotate [--no-reload] # add new verifying kid (zero-downtime)
618
+ supython keygen activate <kid> [--no-reload] # promote kid to active signer
619
+ supython keygen prune [--force] [--no-reload] # drop retired kids past grace window
620
+ supython secret status # show symmetric secret manifest
621
+ supython secret rotate <storage|oauth> # add new verifying symmetric secret
622
+ supython secret activate <storage|oauth> <kid>
623
+ supython secret prune <storage|oauth> [--force]
624
+ supython password rotate <role> # rotate a Postgres role password
625
+
626
+ # Admin
627
+ supython admin create-user # bootstrap the first admin (interactive)
628
+
629
+ # Realtime
630
+ supython realtime enable public.messages # opt a table into realtime
631
+ supython realtime enable public.posts --owner-column author_id
632
+
633
+ # Jobs & worker
634
+ supython worker run --queue default # start the job worker (blocks)
635
+ supython jobs list # list queued/running/finished jobs
636
+ supython jobs show <uuid> # show job details
637
+ supython jobs cancel <uuid> # cancel a job
638
+ supython jobs retry <uuid> # re-queue a failed job
639
+ supython jobs enqueue send_welcome_email --payload '{"email":"a@b.com"}'
640
+ supython cron list # list registered crons
641
+ supython cron sync # sync crons with pg_cron
642
+
643
+ # Test suite
644
+ supython test up # start test Postgres on port 54323
645
+ supython test run [PYTEST_ARGS...] # bootstrap + run full suite
646
+ supython test run tests/unit # fast loop, no Docker
647
+ supython test run tests/integration -k auth_signup
648
+ supython test down # stop test DB (keeps volume)
649
+ supython test reset # stop test DB + delete volume
650
+ ```
651
+
652
+ ## How the auth ↔ PostgREST contract works
653
+
654
+ 1. supython signs an RS256 JWT with its private key and the claims:
655
+ ```json
656
+ {
657
+ "sub": "<user uuid>",
658
+ "email": "<user email>",
659
+ "role": "authenticated",
660
+ "aud": "authenticated",
661
+ "iat": ...,
662
+ "exp": ...
663
+ }
664
+ ```
665
+ 2. The client sends the token to **either** supython (for `/auth/v1/user`)
666
+ **or** PostgREST (for everything else) as `Authorization: Bearer <jwt>`.
667
+ 3. PostgREST verifies the signature via the public JWKS, switches
668
+ the DB role to `authenticated`, and sets `request.jwt.claims` so
669
+ `auth.uid()` returns the user's id inside RLS policies.
670
+ 4. RLS policies on `public.todos` use `auth.uid()` to scope every query.
671
+
672
+ This is exactly the model Supabase uses, in ~400 lines of Python.
673
+
674
+ ## Docker image
675
+
676
+ The supython service ships as a multi-arch image (`linux/amd64`,
677
+ `linux/arm64`) on `python:3.11-slim`. The admin UI bundle is committed
678
+ into the wheel, so the image build is Node-free.
679
+
680
+ ```bash
681
+ # build locally (host arch)
682
+ docker build -t supython:dev .
683
+
684
+ # build the multi-arch manifest
685
+ docker buildx build --platform linux/amd64,linux/arm64 -t supython:dev .
686
+
687
+ # run against an existing Postgres
688
+ docker run --rm -p 8000:8000 \
689
+ -e DATABASE_URL=postgres://supython:supython@host.docker.internal:54322/supython \
690
+ -e JWT_PRIVATE_KEY_PATH=/run/secrets/jwt.pem \
691
+ -v ./.supython:/run/secrets:ro \
692
+ supython:dev
693
+ ```
694
+
695
+ The container runs as a non-root `supython` user (uid 1000), uses `tini`
696
+ as PID 1, exposes port 8000 (override with `SUPYTHON_PORT`), and ships a
697
+ `/livez` HEALTHCHECK. CI builds both arches on every PR via
698
+ `.github/workflows/docker.yml` and publishes the multi-arch manifest to
699
+ GHCR (`ghcr.io/<owner>/supython:<tag>`) on `v*` tags.
700
+
701
+ ## Running the test suite
702
+
703
+ The test suite is split in two so unit feedback is fast and integration
704
+ runs are deterministic:
705
+
706
+ ```
707
+ tests/unit/ # pure Python, no Docker (~6s) — run first, always
708
+ tests/integration/ # full ASGI + Postgres on port 54323
709
+ ```
710
+
711
+ **One-time setup:**
712
+
713
+ ```bash
714
+ supython test up # start dedicated test Postgres on port 54323, apply migrations
715
+ ```
716
+
717
+ **Day-to-day:**
718
+
719
+ ```bash
720
+ supython test run # bootstrap + run full suite
721
+ supython test run tests/unit # fast loop, no Docker required
722
+ supython test run tests/integration -k auth_signup
723
+ pytest tests/unit # unit-only without going through CLI
724
+ ```
725
+
726
+ `supython test run` forwards every extra arg to `pytest`, sets
727
+ `DATABASE_URL` to the test container, and exits with pytest's status code.
728
+ Integration tests skip cleanly (not fail) when Postgres is unreachable, so
729
+ unit tests always run in isolation.
730
+
731
+ **CI:** runners with Docker run `supython test up && supython test run`;
732
+ runners without Docker run `pytest tests/unit` for a meaningful subset.
733
+
734
+ ## Roadmap [shipped v0.1.0]
735
+
736
+ - ✅ Email/password auth, PostgREST contract, RLS demo
737
+ - ✅ OAuth, password reset, magic link, OTP, reuse detection, email backend, test suite
738
+ - ✅ 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`
739
+ - ✅ Realtime over `LISTEN/NOTIFY` with RLS-aware fan-out; Phoenix Channels wire format; broadcast + presence; `examples/chat.html` demo
740
+ - ✅ Job queue worker + `pg_cron` scheduling + hooks + CLI management commands
741
+ - ✅ Grooming + security foundation: unified versioning, CORS closed by default, RS256 JWT, rate limiting, `supython doctor`, pool sizing, statement timeout
742
+ - ✅ Production observable: structured JSON logs, `/livez`/`/readyz`/`/health`, security headers, input size guards, audit log completeness, OAuth PKCE, secret rotation runbooks
743
+ - ✅ (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
744
+ - *(deferred)* — Realtime v2 over logical replication
745
+ - v0.1.0 Release — final sweep, tag, publish wheel, production deployment with no patches
746
+ - ✅ **TypeScript SDK** — `@supython/sdk` wrapping `@supabase/postgrest-js` + `@supabase/realtime-js`
747
+
748
+ ### Post v0.1.0
749
+
750
+ - **v1.1+** — Admin control plane polish (backend + frontend shipped in v0.1.0; tests + remaining DoD items deferred)
751
+ - **Realtime v2** — logical replication (demand-driven; swap when trigger overhead or >8KB payload data warrants it)
752
+ - **Prometheus `/metrics`** + **OpenTelemetry** — optional extras
753
+
754
+ ## License
755
+
756
+ MIT