Build FastAPI endpoints in the layered router → service → Supabase pattern, with Pydantic v2 schemas, JWT/admin gating, and consistent HTTPException-based domain errors. Use when adding/modifying any endpoint under backend/.
中文简介正在准备中。当前显示原文说明。
Supabase Postgres + Storage + Auth operations from FastAPI — service-role client, RLS-aware queries, additive SQL migrations under backend/db/migrations, the create_order RPC, and the product-images bucket. Use when adding tables/columns, writing queries, calling RPCs, or working with Storage.
中文简介正在准备中。当前显示原文说明。
查看安装方法安装前可以查看交给 AI 代理的指令内容。
Version: 1.0 | Working Directory:
backend/
Supabase operations for $ARGUMENTS in this FastAPI service.
One database, three surfaces:
products, orders, order_items, plus the public.create_order PL/pgSQL RPCproduct-images bucket (public read; writes via service-role)app/auth.py (JWKS), bootstrap is manual via the Supabase dashboardAll access from the backend uses the service-role client, which bypasses RLS by design (the service is the single trusted writer). Anonymous browser access is governed by RLS policies defined in the migration.
| # | Rule | Why |
|---|---|---|
| 1 | Use get_supabase() from app.supabase_client — never construct a client inline | One configured service-role client |
| 2 | Multi-row atomic writes go through a Postgres function (RPC) — checkout uses public.create_order | Python loops are not transactional; SQL is |
| 3 | Schema changes ship as new files in backend/db/migrations/000N_<change>.sql — never edit a shipped migration | Migrations are append-only history |
| 4 | Public reads filter is_active = true in the service when the call is for the storefront | Inactive products must not leak |
| 5 | RLS stays restrictive — products SELECT public, all writes service-role; orders / order_items no anon policies | Backend is the security boundary |
| 6 | Storage writes use service-role; the bucket is public-read so URLs work in the storefront without signing | Don't expose service-role to the browser |
| 7 | Never log or return the service-role key | Secrets stay in env / config |
| 8 | No SQLAlchemy / Alembic / MongoDB / Redis in this project — Supabase Postgres is the database | Don't add a second DB stack |
| 9 | Don't widen RLS without an explicit migration + approval | RLS changes are security-critical |
| 10 | For data backfills, write SQL (one-off file under backend/db/migrations/ or a scripts/ script that uses the service-role client) | Reviewable, replayable |
create_order RPCThere's only one — Postgres via Supabase. Use Storage for product images, Auth for admin sessions.
from app.supabase_client import get_supabase
# SELECT with filter, search, pagination
sb = get_supabase()
resp = (
sb.table("products")
.select("*", count="exact")
.eq("is_active", True)
.ilike("name", f"%{q}%")
.order("created_at", desc=True)
.range(offset, offset + limit - 1)
.execute()
)
items, total = resp.data or [], resp.count or 0
# INSERT / UPDATE / DELETE
sb.table("products").insert(data.model_dump()).execute()
sb.table("products").update(payload).eq("id", str(product_id)).execute()
sb.table("products").delete().eq("id", str(product_id)).execute() # we don't expose this in the API — use is_active=false instead
# RPC (atomic checkout)
resp = sb.rpc("create_order", {
"p_email": email,
"p_shipping_name": shipping_name,
"p_shipping_address": shipping_address,
"p_items": items,
}).execute()
# Storage upload (service-role, public-read bucket)
sb.storage.from_("product-images").upload(
path=key,
file=file_bytes,
file_options={"content-type": mime, "upsert": "true"},
)
public_url = sb.storage.from_("product-images").get_public_url(key)
backend/db/migrations/0001_init.sql)| Table | Notable columns | Notes |
|---|---|---|
products | id uuid pk, slug text unique, name, price_cents int, image_url, stock, is_active bool, created_at, updated_at | RLS: SELECT public when is_active=true; writes service-role only |
orders | id uuid pk, email, shipping_name, shipping_address jsonb, subtotal_cents, total_cents, status text check ('pending','paid','shipped','cancelled') | RLS: no anon policies |
order_items | id, order_id fk, product_id fk, quantity int, unit_price_cents int | Snapshot price at purchase; RLS: no anon policies |
Function:
public.create_order(p_email, p_shipping_name, p_shipping_address, p_items) — locks affected products rows, validates stock, inserts the order + items, decrements stock, marks the order paid, returns {order_id, total_cents, status}.Migrations are plain SQL files in backend/db/migrations/, applied in order. There's no migration tool wired in for this MVP — files are run manually against Supabase via the SQL editor or psql.
| Rule | Why |
|---|---|
| One concern per file — schema vs data, not mixed | Easier to roll forward / back |
Filename: 000N_<change>.sql continuing from 0001_init.sql | Order is preserved |
| Additive on populated tables — add nullable column → backfill → enforce in a follow-up migration | Avoids downtime / breakage |
| Wrap in a transaction when changes span multiple statements that must succeed together | Atomicity |
| Update RLS in the same migration that adds the table | Don't ship a table with no policies and forget |
| Never edit a shipped migration | Replays must be deterministic |
-- backend/db/migrations/0002_add_product_brand.sql
begin;
alter table public.products
add column if not exists brand text;
create index if not exists products_brand_idx on public.products(brand);
commit;
create_order RPC (template)-- backend/db/migrations/000N_update_create_order.sql
begin;
create or replace function public.create_order(
p_email text,
p_shipping_name text,
p_shipping_address jsonb,
p_items jsonb
) returns jsonb
language plpgsql
security definer
as $$
declare
-- ... (mirror the existing fn structure: lock rows, validate stock, insert, decrement, set status='paid')
begin
-- body
end;
$$;
commit;
The RPC is security definer so it runs with the function owner's privileges — that's how anonymous checkout writes orders/items even though orders has no anon RLS policies. Don't change security semantics without explicit approval.
-- backend/db/migrations/000N_backfill_brand.sql
begin;
update public.products
set brand = split_part(name, ' ', 1)
where brand is null;
commit;
For large tables, batch in chunks; for now we have small data so a single statement is fine.
# preferred — Supabase SQL editor (paste the file, run)
# or, with the Postgres connection string from the Supabase dashboard:
psql "$SUPABASE_DB_URL" -f backend/db/migrations/0002_add_product_brand.sql
Bucket: product-images (public read).
# backend/app/services/storage.py — exemplar
def upload_product_image(file: UploadFile) -> str:
sb = get_supabase()
key = f"{uuid4()}-{file.filename}"
sb.storage.from_("product-images").upload(
path=key,
file=file.file.read(),
file_options={"content-type": file.content_type or "application/octet-stream", "upsert": "true"},
)
return sb.storage.from_("product-images").get_public_url(key)
Rules:
products.image_urlproducts — SELECT allowed for anon when is_active = true; INSERT/UPDATE/DELETE denied for anon (service-role bypasses)orders, order_items — no anon policies; only service-role and the security definer create_order function can writeIf a query unexpectedly returns 0 rows or 401/403 from the anon side, RLS is almost always the cause. From the backend (service-role) it should never matter — if it does, you're using the wrong client.
create_order. Fix: call the RPC..eq("is_active", True). Fix: add the filter; consider adding a service helper that always applies it for public reads.SUPABASE_SERVICE_ROLE_KEY and SUPABASE_URL.orders works (somehow) → an anon RLS policy was added by mistake. Audit policies; remove via a new migration.productsbackend/db/migrations/000N_<change>.sql (additive)app/schemas/product.py to include the field (optional on ProductCreate if backfilling)create table, indexes, RLS policies (default to anon-deny), commentsapp/schemas/app/services/routers/*.pycreate or replace function public.create_order(...)app/services/orders.py if input/output shape changesapp/schemas/order.py to mirror the new shapeorders, order_items, stock decrementedupdate/insertbackend/scripts/ (see seed_shirts.py) and the service-role clientbackend/db/migrations/0001_init.sql — schema + RLS + RPCbackend/db/seed.sql — seed databackend/app/supabase_client.py — service-role client factorybackend/app/services/products.py — query + write patternsbackend/app/services/orders.py — RPC callbackend/app/services/storage.py — Storage uploadbackend/scripts/seed_shirts.py — data import via service-role还没有评价。欢迎分享你的使用感受。
无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。
还没有评论。来写第一条吧。
Build FastAPI endpoints in the layered router → service → Supabase pattern, with Pydantic v2 schemas, JWT/admin gating, and consistent HTTPException-based domain errors. Use when adding/modifying any endpoint under backend/.
中文简介正在准备中。当前显示原文说明。
pytest + httpx + TestClient patterns for the FastAPI backend. Dependency overrides for auth, FakeSupabase mocking, HTTPException assertions. Use when writing or reviewing backend/tests/test_*.py.
中文简介正在准备中。当前显示原文说明。
Drive Chrome via the claude-in-chrome MCP to exercise plan.md::Verify steps for a shipped feature. Loads deferred MCP tool schemas, asserts console/network cleanliness, records a GIF for the PR. Used by the browser-verifier agent.
中文简介正在准备中。当前显示原文说明。
Pre-PR code review checklist for this repo. Plan alignment, security gates, layering, dead code, API-surface completeness. Used by the code-reviewer agent.
中文简介正在准备中。当前显示原文说明。
Integrate with the FastAPI backend through the typed wrapper in frontend/lib/api.ts, attach Supabase JWTs for admin calls, and handle errors via ApiError. Use when adding/modifying API calls or wiring auth.
中文简介正在准备中。当前显示原文说明。
Build pages and components in the Next.js 15 App Router (RSC by default, "use client" only when needed), Tailwind for styling, typed props, and the existing CartProvider for cart state. Use when creating or modifying pages, layouts, components, or forms in frontend/.
中文简介正在准备中。当前显示原文说明。