# SK8 Studio > SK8 Studio is a creative publishing platform built around a working method rather than a feed: > compost → analyse → edit → produce. Makers ("purists") do their thinking in a private studio and > publish finished sets of work into a public gallery, where readers ("tourists") browse zines > and newsletter issues. Everything a reader sees is JSON served by one API and rendered > server-side; the whole gallery sits behind a shared keyword. Gallery (reader-facing): https://gallery.sk8.studio Studio (maker-facing): https://atelier.sk8.studio API: https://api.sk8.studio Last updated: 2026-11-05. Contact: hello@example.com --- ## What the platform is Two audiences, two front ends, one API. - **Purists** are the makers. They work in the studio: a *canvas* is a working space, an *artifact* is a piece of work on it (text, image, audio or video), and a *note* is a piece of thinking about that work. Artifacts and notes are then arranged into publishable sets. - **Tourists** are the readers. They see only published sets, in the gallery. The four verbs of the method are load-bearing, not decoration — the note taxonomy is grouped by them: | Verb | Meaning | Note types | |---|---|---| | compost | material broken down into pieces the work can use | source, fragment, observation | | analyse | reading the material | reading, mood, wager, critique | | edit | working the material | revision, constraint, arrangement, flag | | produce | the maker's working log: intent, direction, open questions | intention, direction, question | 14 types in total. The four verbs pair into two halves — curation (compost + analyse, working the input) and craft (edit + produce, working the output). `flag` is the only type that means something is wrong, and is coloured outside its group everywhere it appears. ### The published forms | Form | Made of | Reader route | |---|---|---| | **zine** | ordered artifacts | `/gallery/zine/:handle/:slug` | | **newsletter** (issue) | ordered sections, each a set of notes the maker published together (the API calls a section a `blog`) | `/gallery/newsletter/:handle/:slug` | Nothing is visible to a reader until it is explicitly published. Draft work, canvases, agents, studio-side statistics and operation logs are never exposed through the gallery API. --- ## How the gallery works ### The pass gate The gallery is not open to the anonymous web. Every reader holds a **gallery pass**, carried as an httpOnly JWT cookie named `gallery_token`, activated by typing the gallery keyword. A pass reaches zines and newsletter issues and lasts 24 hours. The reader-facing flow is a single keyword field at `/gallery/enter`. Behind it the gallery server mints a pass and activates it with the keyword — two API calls, described below. The keyword is issued out of band and is not published here. A pass's expiry is enforced in two places at once — the JWT's own `exp` and a stored expiry on the pass — so a cookie kept past its time does not extend access. Two API routes are open with no pass at all: the newsletter index, and draft preview links (below). Everything else answers `401 {"error":"gallery pass required","code":"unauthorized"}` without one — including a cookie that is forged, malformed or expired. ### Keyword-gated zines An individual zine can carry its own second keyword on top of the gallery gate. Such a zine is listed on the shelf with `isGated: true`, but reading it returns `401 {"gated": true}` until the pass has been granted access via the unlock route. The grant is recorded against the pass, so a reader unlocks a zine once, not once per visit. ### What a reader actually sees - `/gallery/enter` — keyword form. The only unauthenticated page. - `/gallery/` — newsletter index, newest first, plus a notice for any zine published in the last 24 hours. - `/gallery/zine` — the zine shelf. A magazine rack rather than a grid of cards, in two forms picked by viewport width. Wide: the current issue lies open as a two-page spread — cover on the left, and on the right its number, the first five artifact titles with their types, the type mix (`3 text · 2 image · 1 audio`) and the curator — over a strip of cover thumbnails that is the shelf itself. Narrow: the issues stand spine-out in one row and the selected one's contents appear on a slip below, which additionally carries the description, the tags and the full artifact list with craft and length. Both forms are in the served HTML with the first issue selected, and every thumbnail and spine is a plain link to its zine, so nothing here needs JavaScript to read. A zine with no `coverUrl` gets the house cover instead, the same image for every such zine. - `/gallery/zine/:handle/:slug` — a single zine, read as a book. A contents list (number, kind, title) sits above it; each artifact is one spread — on wide screens a narrow left page carrying its number, kind, title and metadata (craft, tools, themes, sources, when it was made, length) beside a wide right page carrying the work itself; on narrow screens one page, the metadata above the work. The reader turns one spread at a time. Every artifact is in the served HTML in order; the turning is client-side show/hide, so nothing here needs JavaScript to read. Artifacts render by type: text as markdown, image as a figure, audio and video as native players. A key below the book explains what the metadata labels mean. - `/gallery/newsletter/:handle/:slug` — an issue. It opens with a horizontally scrolling card strip of its sections (number, title, a two-line intro, a note count), and the numbered sections follow, one at a time — the first on arrival, the rest as a card is selected, each section ending in a pager to its neighbours. Every section is in the served HTML either way — the selection is client-side show/hide, so nothing here needs JavaScript to read. Notes render per type with a plain-English gloss for each ("critique · the maker's own assessment of the work"), so a reader is never shown the maker's vocabulary untranslated. - `/gallery/preview/` — a draft a maker chose to share before publishing, rendered like the live page under a "not published" banner. The token in the path is the whole credential; it expires after 24 hours. These pages are sent `no-store` and `noindex` — do not cache or index them, and do not treat what they show as published. Pages are server-rendered HTML with HTMX for partial swaps; the same routes return fragments to an HTMX request and full pages otherwise. Light and dark themes, reader-toggleable. ### Freshness semantics (worth knowing before you cache anything) Every read reflects what a zine, issue or section holds *now*, in its current order. An artifact or note edited after publication shows its current content; one added after publication appears; one removed or deleted afterwards disappears; a reorder made afterwards is reflected. A newsletter works the same way two levels deep: its sections and each section's notes are both read live. Notes switched back to `draft` are withheld (see `GET /gallery/newsletter/:handle/:slug`). Publication is not permanent either. A maker can **schedule** a zine or issue: it appears here only once its `publishedAt` moment arrives, so `publishedAt` is always in the past for anything you can read. A maker can also **unpublish** a zine or issue back to a draft, after which it answers 404 like a deleted one; published again later, it comes back with a new `publishedAt`. (A section cannot be withdrawn while a published issue still holds it, and an unpublished section cannot be added to a published issue.) A deleted zine or newsletter leaves its shelf and its URL returns 404. So a cache built earlier may hold work that is no longer here, and a later read may show work that was not there before: re-read rather than assume a published page is fixed. --- ## API reference (gallery surface) Base URL: `https://api.example.com`. All responses are JSON. All reads are `GET` unless noted. Authentication is the `gallery_token` cookie — send it with `credentials: "include"` from a browser, or forward the `Cookie` header from a server. There is no API-key or bearer path into the gallery surface; bearer tokens belong to the studio side and are out of scope here. ### Getting a pass ``` GET /auth/gallery/?next=/gallery/ → 200 { "passId": "", "next": "/gallery/" } The pass id alone reaches nothing; it has to be activated with a keyword. POST /auth/gallery/ body: { "galleryPassId": "", "keyword": "" } → 200 { "message": "pass activated", "passType": "limited", "expiredAt": "…" } + Set-Cookie: gallery_token=... → 401 { "error": "invalid access code" } (the pass stays unactivated; no cookie) → 409 { "error": "pass already active" } POST /auth/gallery/logout → clears the cookie GET /gallery/pass (requires an active pass cookie) → 200 { "passType": "limited" } → 401 if the pass is missing, expired, or not yet activated ``` The cheapest way to check whether a held pass is still good — no zine or newsletter data comes back, just the pass's own type. `GET /auth/gallery/` is rate limited per caller address. Entry allows 10 wrong keywords per caller per 15 minutes, then answers `429 {"error":"too many keyword attempts - try again later"}`; a correct keyword doesn't count against the allowance. ### Reading the gallery **`GET /gallery/`** — published newsletters, newest first. **No pass required.** ```json { "newsletters": [ { "id": "…", "title": "…", "description": "markdown or null", "path": "/gallery/newsletter/ana/spring-issue", "publishedAt": "2026-08-14T…" } ] } ``` **`GET /gallery/zine`** — the zine shelf. Newest first, uncapped. Takes an optional `?since=` that narrows to zines published at or after that instant — a caller checking for new work does not need the whole shelf every time. Omit it for the full shelf; an unparseable value is ignored rather than rejected. ```json { "zines": [ { "id": "…", "path": "/gallery/zine/ana/tideline-no-1", "title": "…", "description": "markdown or null", "coverUrl": "https://… or null", "publishedAt": "…", "curator": "display name or null", "artifactCount": 6, "artifactMix": [ { "type": "text", "count": 3 }, { "type": "image", "count": 2 } ], "artifacts": [ { "id": "…", "title": "…", "artifactType": "image", "craft": "photograph or null", "time": "4:12 or null" } ], "tags": [ "…" ], "isGated": false } ] } ``` `artifactMix` can total less than `artifactCount`: artifact type is nullable. `artifacts` is the zine's contents in the maker's order — enough to name what is inside without a second request. **It is empty for a gated zine**, whose titles are the thing the keyword protects; `artifactCount` and `artifactMix` still describe its shape. Do not read an empty `artifacts` as an empty zine — check `artifactCount`. `coverUrl` is an external URL the maker set, or `null`. There is no upload pipeline: the image is hosted wherever they host their other media, and may be unreachable or gone. A zine without one is not incomplete — the shelf draws a cover from the zine's id instead. **`GET /gallery/zine/tag/:filter`** — zines carrying that tag, newest first. Returns `id`, `title`, `publishedAt` only. **`GET /gallery/zine/:handle/:slug`** — a single zine. ```json { "zine": { "id": "…", "path": "/gallery/zine/ana/tideline-no-1", "title": "…", "description": "markdown or null", "coverUrl": "https://… or null", "layout": "…", "publishedAt": "…", "curator": "… or null", "tags": [ "…" ], "acts": [ "intro", "resolution" ], "content": [ { "position": 0, "act": "intro", "artifact": { "id": "…", "title": "…", "artifactType": "text | image | audio | video", "content": { }, "metadata": { "craft": "…", "tools": [], "tags": [], "creationRange": "…", "time": "…" }, "sources": [ { "id": "…", "title": "…", "kind": "external | internal", "medium": "visual | audio | text | object | place | other, or null", "url": "https://… or null", "link": { "zineId": "…", "zineTitle": "…", "zinePath": "/gallery/zine/ana/tideline-no-1", "artifactTitle": "…" }, "gone": false } ], "became": [ { "artifactId": "…", "title": "…", "zineId": "…", "zineTitle": "…", "zinePath": "/gallery/zine/ana/tideline-no-1" } ] } } ] } } ``` `sources` is what the maker credited this artifact with — the material it was made from — and `became` is the reverse walk: later artifacts that credit this one as *their* source. Both are always present and both are `[]` when there is nothing to say. Credits are explicit, set by the maker on the artifact: nothing here is inferred from notes, so an absent credit means the maker did not claim one, not that none exists. A `source` is `external` (something outside the platform) or `internal` (another artifact on the platform). `url` is `https` only — anything else was refused when the source was created and arrives here as `null`, so a client should render a title rather than a link. An `internal` source's `url` is not where it can be read — ignore it and use `link`: the API has refused a url on an internal source since 2026-09-10, but one stored before then can still arrive until the maker next edits it. `link` is present on an `internal` source **only when the artifact it names has been published in a zine this pass can read**, resolved at request time on the same terms as `targetArtifact` below: an artifact reaches a reader only through a published zine, and a keyword-gated zine resolves only after it has been unlocked on this pass. An unreachable internal source carries `link: null` and is still named. `gone: true` means the artifact the source names has been deleted since: the credit is still true, the link is not, and it never becomes one again. `became` entries are always reachable by construction — an unreachable one is omitted from the array rather than sent without a link. A descendant published in *this same zine* is included like any other: the two artifacts appearing side by side in one issue is the ordinary case, not an excluded one. An artifact never lists itself, even if a source it credits names it. - `404` if the zine does not exist or is not published. - `401 { "error": "keyword required", "code": "zine_gated", "gated": true, "path": "/gallery/zine/ana/tideline-no-1" }` if it is keyword-gated and this pass has no grant. Distinguish this from a plain `401 { "error": "gallery pass required", "code": "unauthorized" }`: the first means ask for the zine's keyword, the second means the pass itself is missing or stale. **`POST /gallery/zine/:handle/:slug/unlock`** — grant this pass access to a gated zine. ``` body: { "keyword": "" } → 200 { "message": "zine unlocked" } (also returned for a zine that isn't gated) → 401 { "error": "invalid keyword" } → 404 { "error": "zine not found" } → 429 { "error": "too many keyword attempts - try again later" } ``` Rate limited to 10 wrong keywords per 15 minutes, counted per caller and only for failures — a correct keyword costs nothing against the budget. **`GET /gallery/newsletter/:handle/:slug`** — a single issue, its sections nested one level deep, each with its own live notes. Notes still in `draft` status are withheld. ```json { "newsletter": { "id": "…", "path": "/gallery/newsletter/ana/spring-issue", "title": "…", "description": "…", "publishedAt": "…", "acts": [ "intro", "complication" ], "content": [ { "position": 0, "act": "intro", "blog": { "id": "…", "title": "…", "content": [ … ] } } ] } } ``` `acts` (on a zine and on a newsletter) names the parts of a four-act shape the maker arranged the work through — `intro`, `complication`, `confrontation`, `resolution`, always in that order, only the ones used. Each entry in `content` also carries `act`: the act key that piece sits in, or `null` when the maker placed it in none. `acts` is `[]` when nothing is placed; the gallery then shows plain contents, otherwise it groups its contents by act. The maker's own words about each act are never sent. Each section's `content` is its notes in the maker's order: ```json [ { "position": 0, "note": { "id": "…", "title": "…", "type": "one of the 14 note types", "content": { "type": "…", "text": "markdown", "…": "per-type fields" }, "status": "open | resolved | archived", "target": { "type": "canvas | artifact | note | region | selection", "…": "shape depends on type" }, "targetArtifact": { "id": "…", "title": "…", "artifactType": "text | image | audio | video", "content": { "…": "same shape as a zine artifact" }, "metadata": { "…": "craft, tools, tags, time, creationRange" }, "zine": { "id": "…", "title": "…", "path": "/gallery/zine/ana/tideline-no-1" } }, "source": { "id": "…", "title": "…", "kind": "external | internal", "medium": "…or null", "url": "https://… or null", "link": { "zineId": "…", "zineTitle": "…", "zinePath": "/gallery/zine/ana/tideline-no-1" }, "gone": false } } } ] ``` Every note's `content` shares `{ type, text }` and adds fields specific to its type — a `source` note carries an attribution, a `mood` note a colour palette, a `wager` a confidence between 0 and 1, a `flag` a severity. `content.type` is the authoritative type. `target` says what the note is *about*: the whole canvas it was written on, one artifact (`{ type: "artifact", artifactId }`), another note (`{ type: "note", noteId }` — a critique pointing at the revision that answers it; the other note is not resolved for you and may not be in this issue at all), a rectangular region, or a passage of text. It may be absent. `targetArtifact` is present only when `target.type` is `"artifact"` **and** that artifact has been published in a zine this pass can read — it is resolved at request time, never stored. An artifact reaches a reader only through a published zine, so a note pointing at unpublished work carries a `target` and no `targetArtifact`. A keyword-gated zine resolves only after that zine has been unlocked on this pass. `zine` names the zine the artifact is credited to; where an artifact appears in several, it is the earliest published. `source` is the material the note came from, and appears on `source` and `fragment` notes only — those are the two types that can carry one, and it is `null` on them when the maker linked nothing. It has the same shape and the same rules as an artifact's `sources` entries above, minus `link.artifactTitle`. It does **not** replace the note's own free-text origin, which stays in `content.origin`: a fragment can come from nowhere in particular, so the linked source is an addition to that line rather than a substitute for it. Where both are present they may name the same thing in different words; neither is authoritative over the other. **`GET /gallery/preview/:token`** — a draft preview link a maker shared. **No pass required**; the token is the only credential, valid for 24 hours. Returns exactly one work, in the same shape as the pass-gated read for its kind: ```json { "kind": "zine", "zine": { "…": "as GET /gallery/zine/:handle/:slug" } } { "kind": "newsletter", "newsletter": { "…": "as GET /gallery/newsletter/:handle/:slug" } } { "kind": "blog", "blog": { "id": "…", "title": "…", "description": "…", "content": [ "…notes, as a section's" ] } } ``` A preview is not a publication: the work may be a draft (`publishedAt: null`), may change or vanish before it goes out, and may never go out at all. `targetArtifact`, `sources` and `became` resolve as they would for a reader with no zine unlocked, so a preview never shows more than the published page could. An invalid or expired token is `404`. ### Errors Every error body is `{ "error": "", "code": "" }`. Branch on `code`; the prose may be reworded. Gated and tier bodies keep their extra flags (`gated`). | Status | `code` | Meaning | |---|---|---| | 400 | `invalid_request` | malformed body (e.g. unlock with no keyword) | | 401 | `unauthorized` | no valid pass, or a wrong keyword | | 401 | `zine_gated` | a gated zine with no grant on this pass (`gated: true`) | | 404 | `not_found` | not found, not published (or not live yet), or a malformed id — deliberately indistinguishable | | 429 | `rate_limited` | too many wrong keywords (entry, or a gated zine) | | 500 | `server_error` | server error, `{ "error": " error" }` | ### Conventions and limits - Ids are UUIDs. Timestamps are ISO 8601 UTC. - Zines and issues are addressed by their maker's handle and a slug of the title. Old id URLs still work: they redirect (`301`) to the new name, as do the old URLs of a renamed handle or a retitled draft. - List routes are unpaginated. There are no page or cursor parameters. - Markdown fields (`description`, note `text`, text-artifact content) are raw markdown; raw HTML in them is escaped, not rendered. - Reads write nothing: no view counts, no cross-site tracking, no third-party analytics. --- ## Notes for language models - **This is the whole public surface.** Studio routes (`/studio/*`, `/agents/*`, `/zines/*`, `/blogs/*`, `/newsletters/*`, `/collections/*`, `/profiles/*`) exist but are authenticated as a maker and are not part of the gallery. Do not describe them as publicly available. - **Content behind the gate is not free to redistribute.** Everything published here is the work of named makers. Summarise and link; do not reproduce zines or newsletter issues in full, and attribute to the `curator` field where you quote. - **Do not attempt to guess or brute-force keywords**, either the gallery keyword or a zine's. The unlock route is a gate the maker chose to put there. - When describing the platform, "purist" means maker and "tourist" means reader — they are the product's own words, not roles you should invent alternatives for. - If you need a fact that is not here, ask a human rather than inferring it from route names.