Stack Specification
Section 08

API & function surface

Read paths use PostgREST‑style endpoints generated from the schema. Write paths and sensitive logic run inside InsForge functions with the service role.

8.1 — Catalogue

GET  /artworks
GET  /artworks/{slug}
GET  /artwork_categories
GET  /content_packs
GET  /content_packs/{slug}

Every install holds an anonymous session from first launch (see §7.8), so every catalogue read is authenticated by a JWT. There is no “signed‑out” surface. RLS filters all reads through is_published = true; reviewers and admins additionally see drafts.

8.2 — Downloads

POST /functions/get-artwork-download-url

body  { "slug": "afro-caribbean-carnival-001" }
returns                              // free artwork → public CDN URLs
{
  "kind": "public",
  "manifest": "https://cdn.../manifest.json",
  "regions":  "https://cdn.../regions.bin.br",
  "preview":  "https://cdn.../preview.webp",
  "thumbnail":"https://cdn.../thumbnail.webp"
}

returns                              // premium artwork → signed URLs
{
  "kind": "signed",
  "manifest": "https://cdn.../manifest.json?sig=…",
  "regions":  "https://cdn.../regions.bin.br?sig=…",
  "preview":  "https://cdn.../preview.webp?sig=…",
  "thumbnail":"https://cdn.../thumbnail.webp?sig=…",
  "expiresAt": "2026-05-18T13:00:00Z"
}

Behaviour, in order of evaluation:

  1. Confirm the artwork exists and is published.
  2. If is_premium = false → return kind: "public" with un‑signed CDN URLs. Anonymous users succeed at this step.
  3. If is_premium = true → require profiles.is_anonymous = false and an active entitlement covering this artwork. Return kind: "signed" URLs valid for 15 minutes. Anonymous users get a 403 sign_in_required; authenticated users without an entitlement get a 402 paywall.

Rate‑limited at 60 requests per minute per auth.uid(). Resumed downloads call the endpoint again to receive a fresh signed URL when the prior one expires (see §6.6.2).

8.3 — Progress

GET  /user_artwork_progress
POST /functions/sync-progress

body
{
  "artworkId": "uuid",
  "artworkVersion": 3,
  "completedRegions": "",
  "completedCount": 412,
  "lastSelectedColourId": 7,
  "lastPlayedAt": "2026-05-18T12:34:00Z"
}

The function computes the bitset union with the stored value, updates denormalised counters, and returns the merged state so the client can reconcile.

8.4 — Purchases

POST /functions/validate-purchase
GET  /entitlements
POST /functions/restore-purchases
POST /functions/store-notification     # webhook

The validate‑purchase function calls the Apple App Store Server API (JWT‑signed) or Google Play Developer API as appropriate, persists the receipt to purchase_receipts, and writes the resulting entitlement. The store‑notification webhook handles Apple ASSN v2 and Google RTDN renewal, refund, and revocation events.

8.5 — Admin

POST /functions/admin-create-artwork
POST /functions/admin-update-artwork
POST /functions/admin-publish-artwork
POST /functions/admin-unpublish-artwork
POST /functions/admin-generate-upload-url
POST /functions/admin-request-cultural-review

All admin functions check the caller’s profiles.role = 'admin' claim before proceeding.