Architecture Decisions
Ce contenu n’est pas encore disponible dans votre langue.
Architecture Decision Records (ADRs) capture why Artist Alley is shaped the way it is. Each entry is a short document with a status, a date, the area it touches, the roadmap phases it informs, and the other ADRs it interacts with. The full convention lives in ADR 0035.
How to read this catalogue
Section titled “How to read this catalogue”- Status tells you whether a decision is in effect (
accepted), still in discussion (proposed), retired (superseded/deprecated), or drafted but not adopted (rejected). Superseded ADRs stay listed as historical record — open them to see what the current replacement is. - Area groups decisions by where they bite — architecture, security, licensing, monetization, process, ux, ops, infrastructure, extensibility.
- Phases are the roadmap phases the decision informs. Clicking a phase chip jumps to the matching section on the roadmap.
- The search box matches IDs, titles, excerpts, and tags. Facets stack intersectively — picking Status: accepted + Area: ux shows only accepted UX decisions.
| ID | Title | Status | Area | Date | Phases |
|---|---|---|---|---|---|
| 0099 | A field can say when it appears, and that is composition rather than authorization An operator could describe when a field should appear and nothing evaluated it, and the surface that would have evaluated it was receiving field values the caller was not entitled to read. This settles both: a stored condition is a form-composition hint with a whole-condition fail-open rule, the parser is one grammar with two conformant implementations, and the composition read path is required to derive readability on the server and to withhold the values behind it. | Accepted | architecture | — | |
| 0098 | Seed assembly is deterministic, and a derived id is a function of what identifies the thing The seed assembler derives post ids from an anchor that does not identify the post, so two different roundups collide on one id and one of them can never be seeded. It also emits timestamps that change between runs, and `posted_at` is copied from one of them — so feed order moves, and a post can drift across the fixture sweep's protection boundary. Assembly must be a pure function of its inputs, and a derived id must be a function of identity. | Accepted | process | — | |
| 0097 | The published archive is an output, and the build refuses to overwrite content it cannot reproduce The dataset build copies the repository's profile over the published archive's manifest, so the archive is an output and the profile is the source of truth. That direction had never been enforced, and the archive had drifted 12,097 values ahead of the profile — a single ordinary build would have destroyed them. The build now compares before it writes and refuses when the destination holds content the source does not. | Accepted | ops | — | |
| 0096 | Icons come from one installed icon set, never hand-pasted SVG Every icon in the web client comes from the installed `@lucide/svelte` package, imported by name. Hand-pasting SVG markup into a component is not an allowed way to add an icon, because it is how a UI accumulates six stroke weights and four visual grammars that no reviewer can see drifting. The dependency has been installed since sprint 23 and is used by three components out of sixty-four that draw icons. | Accepted | ux | — | |
| 0095 | Test-created rows are identified by provenance, never by naming heuristics The persistent dogfood database mixes seeded content with rows left behind by test runs, and they must be separable to sweep one without destroying the other. The separator is provenance — the seeder stamps `acquisition_source` and the upload API never does — because every naming, extension and date heuristic was tried, all of them failed, and one would have deleted real assets. | Accepted | process | — | |
| 0094 | AI provenance is a declaration, not a permission — three states, and extraction can only ever corroborate An AI declaration is the maker's statement, not a permission on the work. Three states plus undeclared rather than a boolean, because assisted and generated are the distinction people argue about and NULL must never be mistaken for a disclaimer nobody made. Extraction may corroborate AI and can never establish its absence. | Accepted | architecture | — | |
| 0093 | Browse and search compose one query — a filter is a filter wherever it appears A filter chosen on the feed and the same filter chosen on the advanced page produce the same query through the same engine and the same gates — so a new filter is written once, and no surface can grow a second, weaker copy of a visibility rule. | Accepted | architecture | — | |
| 0092 | Vocabularies grow from use, and a field declares where it appears A production catalogue has thousands of values per field, so a vocabulary is searched on the server, never shipped whole; a keyword field grows as people use it, gated by one capability; and each field declares which surfaces it appears on rather than every surface guessing. | Accepted | architecture | — | |
| 0091 | A post is the unit of publication — an asset is personal storage A file you upload belongs to you and appears nowhere else until you make a post from it. Collections and browse contain posts only. Publication is always a deliberate act, never a side effect of adding a file somewhere. | Accepted | architecture | — | |
| 0090 | Mature content is a second axis — a rating is not a clearance `sensitivity` answers "who is ALLOWED to see this"; `mature` answers "who has OPTED IN to seeing it". They are orthogonal — a public artwork can be mature and a restricted one need not be — so mature gets its own column, its own qualification predicate, and its own composition point on each plane. It reuses ADR 0020's display machinery rather than inventing a second one. | Accepted | security | — | |
| 0089 | An ordinary read capability is not administrative standing The admin shell opens on a narrower set than "every capability a live tile names". A capability ordinary signed-in users hold — because it gates a public surface — opens its tile but not the door. The gate narrows; the role is never touched. Deciding this at the role would have broken public browsing to fix a UI guard. | Accepted | security | — | |
| 0088 | A representative image is a pointer at an asset, not a bespoke upload Surfaces that need "the image representing this thing" — a collection cover, a team hero — store a nullable pointer at an ordinary asset, gated per-viewer, falling back to a derived default. Not a bespoke upload. The instance-logo endpoint is the precedent that argues against itself: its content-type sniffing, size bounds, storage pin and MRU history all exist because a logo has no asset to point at. The deciding argument is one representation, not cost. | Accepted | architecture | — | |
| 0087 | A consumer of the shared chrome signal composes with it locally; mutating it is a global act The auto-hiding navbar and the browse control bar read one shared `hidden` signal. A consumer that wants to stay visible for its own reason must AND a local term into its own derivation; calling the store's `reveal()` shows *everything*, which is right for a global interaction and wrong for a local one. Nothing recorded the distinction, and a brief written by the planning agent got it backwards. | Accepted | ux | — | |
| 0086 | UI in the browser top layer must outlive its host, by construction Three separate invisible-UI defects came from one mechanism — the browser top layer. A node parented into a modal dialog is correct while that dialog lives and gone the instant it does not, and a dialog can end in two ways of which only one is an event. The invariant: a portalled node's survival is decided by the portal, never by the ordering at its call site, and the test for any such element asserts it is in the DOCUMENT rather than in a store. | Accepted | architecture | — | |
| 0085 | Rich text is sanitised at both boundaries through one policy A rich_text value is the only stored string a client renders as markup, so it is sanitised on write AND re-sanitised on read, both through the single policy in internal/richtext. The API returns pre-sanitised HTML; clients render it verbatim and carry no sanitiser of their own. | Accepted | security | — | |
| 0084 | Every supporting extractor runs, and canonical fields are namespaced per source Until #828 the extract dispatcher stopped at the first extractor whose Supports() said yes — EXIF registers first and claims image/jpeg, so the IPTC and XMP extractors never ran on any JPEG in production. This records the replacement: fan out to every supporting extractor, merge with per-source namespaced canonical fields, and reconcile semantics at the operator's wiring choice rather than in the merge. | Accepted | architecture | — | |
| 0083 | Peers will need to exchange field schemas — the requirement is real and deliberately unbuilt #738 deleted field_set_id, and the reasoning risked reading as "schema sharing was rejected". It was not. Federation transports content by reference and no metadata at all, while federated IIIF manifests already span instances — so two peers can appear in one viewer with field schemas nothing has ever reconciled. This records the requirement, why the deleted column was the wrong shape for it, and what to build instead. | Accepted | architecture | — | |
| 0082 | The Go heap is bounded by the container's own ceiling, and every environment has one The Go runtime does not read cgroup limits, so the collector paced from the live-heap ratio alone and drove the app past its own 4 GiB ceiling into an OOM kill during preview rendering. GOMEMLIMIT is now derived from the cgroup at boot rather than hardcoded, and the base stack carries a ceiling too — the previous CI-only cap meant the failure was reproducible in CI and invisible everywhere else. | Accepted | infrastructure | — | |
| 0081 | Operators override shipped strings, field defaults, and email templates — the last over a restricted context Three locked admin tiles — site text, email templates and upload defaults — are the same question wearing three hats: what may an operator change about content that ships inside the binary? All three become operator-editable. Templates were rejected outright at first and reinstated by the 2026-07-31 amendment: the hazard is Go templates invoking methods, which is fixed by rendering against a flat typed view-model rather than by prohibition. | Accepted | architecture | — | |
| 0080 | CI's fixture is selected by coverage, and a missing coverage class fails the seed Seeding CI's full 1,947-asset catalogue cost 14 minutes of a 20-minute job to feed a 3-minute suite, and cutting it by volume shed the relations the suite exists to test. The fixture is now chosen by what it must cover rather than how large it is, and a catalogue that cannot supply a required coverage class fails the seed instead of quietly producing a suite that passes without exercising it. | Accepted | process | — | |
| 0079 | The feed can contain sized slots, and an unfilled slot becomes ordinary content Ads and premium placement are the same mechanism wearing two labels: a position in the feed that is larger than one tile. This records the primitive once — a slot is a position, a size in grid units, and an ordered list of fill sources — rather than building it twice. The load-bearing rule is that an unfilled slot degrades to ordinary content, because a hole in a grid is far worse than a collapsed banner. | Accepted | ux | — | |
| 0078 | 3D animation clips are addressable independently of their mesh, and rig identity is extracted The viewer can already play animation clips, but only the ones baked into the file it loaded. Making a shared animation library possible needs two things recorded: a clip is addressable independently of the mesh that shipped it, and a rig's identity is an extracted property rather than something inferred at view time. Shared-rig playback is the supported path; cross-rig retargeting is explicitly not promised. | Accepted | architecture | — | |
| 0077 | A stored credential is never returned, and config that can hold one must be typed Three admin-config surfaces stored credentials and two of them handed those credentials back on read. The rule: a stored secret is write-only — the read reports whether one exists, never what it is — and any config structure capable of holding a secret must be a closed typed schema, because a free-form map makes the rule unenforceable by construction. | Accepted | security | — | |
| 0076 | One time-based media player, with frame-scoped review annotation Video and audio already share one player component; this records that decision and extends it. The review surface addresses time in FRAMES, not seconds, annotations are scoped to a frame rather than floating over the asset, the scrubber renders which frames carry annotations, and adjacent frames' strokes ghost behind the current one at a configurable opacity. | Accepted | ux | — | |
| 0075 | A configuration that references a storage object must pin it for as long as it references it Pins have always been entity-owned — an asset, a companion, a job output. The instance logo introduced the first configuration-owned reference, and content-addressing does not retain: `RemovePin` marks an object GC-eligible the moment its last pin drops. Any config holding a reference must hold a pin, with a bounded count, or the reference rots the first time the sweeper runs. | Accepted | storage | — | |
| 0074 | IIIF Change Discovery over the ArchivePub outbox; no IIIF Auth 2.0 ArchivePub gains a IIIF Change Discovery 1.0 feed serialised from the existing outbox, so cultural-heritage harvesters can crawl an instance without implementing ArchivePub. The spec explicitly permits publishing activities about access-restricted content; we reject that permission, because ADR 0064 makes a restricted resource return 404 precisely so its existence stays hidden. IIIF Auth 2.0 is evaluated and declined. | Accepted | extensibility | — | |
| 0073 | Articles (blogs) are posts with a kind, not a new entity Long-form written content ships as a `kind` discriminator on the existing `posts` entity rather than a parallel table, because a new entity would require a sixth independent expression of the visibility rule — the exact defect that produced five production leaks in one week. | Accepted | architecture | — | |
| 0072 | Raw IP addresses are a separately-grantable data class Any admin surface that returns a user's raw IP gates that field on a dedicated `<area>.pii.read` capability, additive to the `<area>.read` that admits the caller to the surface, and omits the field rather than blanking it. | Accepted | security | — | |
| 0071 | The preview ladder is a published contract, not a shared constant The set of raster variants an install generates is operator-configured, so neither the server nor the client may hardcode rung keys. The server publishes the configured ladder and a per-asset `ladder_available` flag; the client asks rather than assumes. | Accepted | storage | — | |
| 0070 | Public user-profile pages, gated by the existing visibility predicate Artist Alley gets public user-profile pages resolvable by username and by ref, showing a display name and avatar plus exactly the assets, posts, and collections the viewer is already allowed to see — reusing the visibility predicate, so no new enforcement plane. Anonymous visibility follows public mode; owners can opt out per the privacy model. A post-by-asset route resolves to the posts that feature an asset. | Accepted | architecture | ||
| 0069 | Preview rendering via headless three.js; Blender demoted to proprietary-format converter 3D preview generation stays fully server-side and async, but the renderer changes: headless three.js driven by Puppeteer renders the turntable and poster, reusing the same code as the interactive viewer so previews are WYSIWYG. Blender is demoted from "renders every 3D thumbnail" to a converter invoked only for proprietary formats three.js cannot parse. Amended 2026-07-27 (#500): Blender is not packaged at all — it left the image entirely (3.64 GB → 1.82 GB) and returns as a plugin, so the three.js worker is the only renderer. Amended 2026-07-29 (#689): "reusing the same code as the viewer" was aspirational — the renderer carried its own loader and rendered every OBJ and every unlit glTF untextured. The load path is now one shared module both surfaces import. | Accepted | architecture | ||
| 0068 | Testing strategy — catch the class, not the instance #475 shipped green — no test caught a dead internal link because the suite walked posts (which work) but never collection→asset→viewer, and the not-found spec only asserted that bogus routes 404, never that real routes don't. The standard: layered coverage that catches a whole class, red-first regression with every fix, robustness over speed. | Accepted | process | — | |
| 0067 | Assets are first-class linkable entities — a standalone /assets/[id] route Every asset tile links to a per-asset URL, but no such route ever existed — clicking an asset inside a collection 404s (#475). Assets get a standalone route, symmetric with /posts/[id], rendering the source-agnostic AssetPlaylist with a single-asset source. The modal playlist stays for in-context browsing; the route is for direct, shareable, reload-safe links. | Accepted | architecture | — | |
| 0066 | Generic SSO / LDAP is not license-gated Self-hosted SAML / OIDC / LDAP move to the free (AGPL) tier — auth is security hygiene, not a paywall. The managed hosted-IdP bridges (Okta / Auth0 / WorkOS / Azure AD) plus SCIM stay the paid aa-sso-premium add-on: that is operational burden we carry, not a security tax. Amends the tier tables in 0017 / 0038 / 0041. | Accepted | architecture | — | |
| 0065 | Featuring is a placement, not a property of the thing featured Two featured mechanisms already exist — a boolean on collections and a polymorphic featured_items table. ADR 0027 would have grown the weaker one toward the stronger one's job. Instead featured_items becomes the single home for curation, gains an audience scope, and collections.featured is removed. The collection tree survives as structure, not as featuring. | Accepted | architecture | ||
| 0064 | Sensitivity gates content, not rows Asset sensitivity is a content-access tier, not a row-exclusion tier. Restricted and embargoed assets remain listable; what is gated is the bytes. The enforcement point is the binary plane, which today has no check at all. | Accepted | security | — | |
| 0063 | Content visibility — one predicate as the single enforcement point Content visibility is decided in exactly one place — the visibility package's predicate — which is spliced into every read path. Anonymous callers see a public tier that now exists in the schema; the authenticated sensitivity rule is deliberately left undecided rather than guessed. | Accepted | security | — | |
| 0062 | Storage backend enumeration contract — ordered, cursor-resumable List Storage backends must expose enumeration that is globally lexicographic by object key and resumable from a cursor. Filesystem depth-first walk order does not satisfy this, so the fs backend prunes and sorts to honour the contract; a shared contract test enforces it for every backend. | Accepted | storage | ||
| 0061 | Admin surface visibility model — public, capability-gated, superuser-only Admin navigation resolves against three explicit tiers — universally public, capability-gated, and superuser-only — with "no capability declared" meaning superuser-only rather than public, because most tiles have not yet been migrated to a read capability. | Accepted | security | — | |
| 0060 | Public read-only demo instance A public read-only demo (demo.artist-alley.org) that runs the release image with writes blocked at the edge, no guessable admin, and a host- side auto-update — so the full feature surface is browsable without exposing a mutable or privileged instance. | Accepted | ops | — | |
| 0059 | v0.1.0 architecture state — the release bundle as a decision set v0.1.0 (2026-07-11) is the first public tag, and it shipped as a bundle of coupled decisions rather than a feature release: org move to Artist-Alley-Org, site split, AGPL-3.0-only + dual commercial relicense, migration squash point #1, Docker-only distribution, and full removal of RS-derived code. This ADR records that bundle in one place so later readers don't have to reconstruct it from seven earlier ADRs, and records v0.1.1 (2026-07-13) as the first append-only patch proving the release train works. | Accepted | architecture | ||
| 0058 | Two-tier demo seed dataset (Layer A public / Layer B private) The demo/seed dataset is split into two tiers. Layer A (site_a) is CC0/CC-BY/public-domain only — safe to redistribute publicly under an aggregate CC-BY-SA 4.0 license — and is the ONLY tier used for anything public: marketing screenshots, the hosted demo, the README, and public CI fixtures. Layer B (site_b) may carry IP-referenced or richer content and is private — dogfood + local testing only, never published. | Accepted | ops | ||
| 0057 | v0.1.0 baseline schema shape The v0.1.0 tag ships a single `00001_baseline_v0_1.sql` migration that collapses the pre-tag chain (baseline_v1 + 28 append migrations) into one canonical schema. This ADR documents the design invariants the baseline captures so future contributors + the eventual append-only-forever rule (per ADR 0046 pending issue #228) have a fixed reference. | Accepted | architecture | ||
| 0056 | Cross-entity BM25-shaped + hybrid-vector search with visibility floor Locks the architecture of the 1.16.B search arc: unified /search endpoint over Postgres tsvector with field weighting, DSL parser with strict whitelist, cross-package visibility.Filter, pgvector hybrid ranking, LISTEN/NOTIFY cache invalidation, saved-searches with delta detection, and admin reindex + observability surface. Ships end-to-end via PRs #174 → #182. | Accepted | architecture | ||
| 0054 | Account lifecycle + email substrate + admin impersonation + self-service 2FA Operators can run a public AA instance: SMTP email substrate + admin impersonation + self-service TOTP 2FA + self-registration with email verification — all built on the existing capability + audit + sessions infrastructure with no architectural regressions. | Accepted | security | ||
| 0053 | IIIF Image API + Presentation API for institutional interoperability Expose every image asset through the IIIF Image API 3.0 and every collection through the IIIF Presentation API 3.0, so cultural-heritage operators can install AA as a Mirador / Universal Viewer-compatible catalogue without custom builds. | Accepted | extensibility | ||
| 0052 | Optimistic-concurrency edit-safety on mutable entities Mutable entities (asset, collection, post) carry an `updated_at` revision token; PATCH endpoints require `If-Unmodified-Since` and reject stale writes with 409 Conflict. Lock-free, federation-safe, replaces the original "resource lock" design from the RS-gap audit. | Accepted | architecture | ||
| 0051 | Artist Alley as a Model Context Protocol (MCP) client Consume external MCP servers as tool sources from AA's AI orchestrator. Inverse of ADR 0050 — instead of AA exposing its catalogue to external agents, AA's own provider abstraction calls out to operator-registered MCP servers (ComfyUI, custom studio bridges, etc.). | Accepted | extensibility | ||
| 0049 | Encrypted federation + two-server local dogfood infrastructure Phase 1.22.I lights up end-to-end encrypted federation (X25519 keypair-per-user, NaCl-box envelope encryption, peer capability negotiation, 7-day key-rotation grace period) — but only after a two-server local dogfood setup runs the 1.22.D wire-protocol surface against itself. Dogfood infrastructure becomes a permanent dev surface in `infra/docker/dogfood/`, not throwaway. | Accepted | architecture | ||
| 0048 | Physical archive mode — accession numbers, loans, provenance Physical archive features (accession numbers, location tracking, loan management, provenance chains, standards interop) ship first-party in-tree as a default-off feature flag — not a premium add-on, not a third-party plugin. Museums, galleries, libraries, archives, and studios with physical prop / costume collections are a real target audience, not a paywallable extension. | Accepted | architecture | ||
| 0047 | Cross-platform packaging — Linux containers, macOS bundles, Windows MSI/service Artist Alley ships through three first-class platforms: Linux (Docker + apt/dnf + static binaries; current shipping channel), macOS (Homebrew + signed .app bundle + DMG, notarized), and Windows (signed MSI installer registering a Windows Service, built via WiX Toolset, with embedded Postgres for the turnkey SMB-studio path). Three personas drive the targeting: SRE / DevOps operator, engineer self-hoster, SMB studio admin. | Accepted | ops | ||
| 0046 | Migration baseline + squash policy Pre-MVP migration sequences may be squashed into a single baseline migration. Squashes are deliberate, audited, and destructive — no upgrade path is supported from the prior sequence. After v1.0 launch, squashes are forbidden; the migration history becomes append-only forever. | Accepted | process | ||
| 0045 | Public demo — ephemeral per-visitor sandboxes artist-alley.org runs a public demo at demo.artist-alley.org that spins up a personal, fully-isolated sandbox instance per visitor. Each sandbox is pre-seeded with CC0 sample assets, auto-destroys after a TTL, and federates with no other instance. Visitors get the real product experience without any cross-visitor moderation surface. | Accepted | ops | ||
| 0044 | Activities ledger — CQRS-lite federation backbone Every state-mutating federated social action emits an Activity in the same database transaction as its domain write. The activities table is the canonical record (source of truth) for federation; the domain tables (posts, comments, likes, user_follows, user_blocks, direct_messages, notifications) are kept in sync synchronously as optimized read projections. CQRS-lite — one ledger, multiple typed projections — without the operational cost of pure event-sourcing. | Accepted | architecture | ||
| 0043 | Federation — artist-alley walled-garden protocol (ActivityPub-shaped, no public fediverse interop at v1) Artist Alley instances federate with each other through an ActivityPub-shaped protocol — but federation is artist-alley to artist-alley only at v1, not to the public fediverse. The protocol is built ground-up in Go, uses plain JSON with a versioned schema (no JSON-LD), Ed25519 signed envelopes, custom activity vocabulary for our domain (Asset / Approve / Annotation / WorkflowTransition / AssetVersion), and CAS-native URIs that dedupe bytes across the network. Trust is per-instance allowlist + optional curated directory. | Accepted | architecture | ||
| 0042 | Distributed catalogs — typed constants per package, no central god-file Named-constant catalogues (field types, status codes, log codes, event types, notification types, permission codes, icons) live in the package that owns them as typed Go constants. No central definitions god-file. A short meta-index in docs/catalogs.md tells contributors where each catalogue lives. | Accepted | process | — | |
| 0041 | Identity provider registry + license-gated enterprise gates Authentication backends, multi-tenancy, and other enterprise-only surfaces register through a license-gated process-wide registry that evaluates feature flags at construction time. Patching the runtime license check in isolation produces nothing, because the registration block never ran for unlicensed features. | Accepted | architecture | ||
| 0040 | Clean-room reverse-engineering methodology Project-wide procedure for producing demonstrably non-derivative implementations of publicly-specified or reverse-engineered functionality — three-phase Spec / Observation / Implementation flow, contributor quarantine, approved spec sources, and per-decision provenance documentation. Applies to any work that lands in a closed-source artifact or that we want to keep license-portable. | Accepted | process | — | |
| 0039 | Native viewers for proprietary DCC formats — clean-room, Blender-augmented, premium Inventory, thumbnail, and interactive viewing for proprietary DCC scene files (`.max`, `.mb`, `.ma`, etc.) ship across three deliberate layers — a free Go metadata reader, a free Blender worker for thumbnails + conversion, and a premium `aa-dcc-viewer` add-on holding the proprietary routing algorithm + native parser + material translation. Clean-room methodology is load-bearing. | Accepted | architecture | ||
| 0038 | Premium add-on layer — paid artifacts on top of an AGPL core Premium add-ons are a paid, proprietary, Ed25519-licensed artifact layer that sits on top of the AGPL core via the existing capability add-on system. They cover the surfaces where operators monetize third parties (commerce, ads, memberships) and where we carry operational burden on their behalf (cloud-bridge AI, premium SSO, DCC plugins, backup / DR). The core stays AGPL and feature-complete for self-hosters; the tier system stays orthogonal; no feature is ever clawed back into a paywall. | Accepted | monetization | ||
| 0037 | Caption & subtitle artifacts — portable, editable, reusable tracks Caption and subtitle tracks are first-class artifacts on audio / video assets — produced by the Whisper transcription add-on, edited by humans through a timeline UI, stored as canonical WebVTT, served as portable VTT / SRT / TTML downloads, and auto-attached to the video viewer as track sidecars. | Accepted | architecture | ||
| 0036 | External imports framework — multi-source, UI-managed asset ingestion A first-class subsystem for pulling assets out of external systems — network shares, S3 buckets, git, Perforce, ShotGrid, OneDrive, SharePoint, Google Drive — through a single connector contract, with UI-managed config, per-item lifecycle tracking, explicit conflict resolution, and ingest-or-reference storage modes. | Accepted | architecture | ||
| 0035 | ADR conventions and documentation pipeline Every Artist Alley ADR ships with YAML frontmatter, follows a strict section structure, and is rendered with an automated cross-reference header (status badge, supersedes trail, related ADRs, related phases) by the docs site pipeline. | Accepted | process | — | |
| 0034 | Capability add-ons — out-of-band heavy components The Go binary should stay small and audit-able in an afternoon. The moment we bake CLIP weights (300 MB), Whisper models (200 MB → 3 GB depending on flavour), Stable Diffusion / Flux / ComfyUI runtimes (multi-GB), or Tesseract trained data (200 MB / language) into the single-b… | Accepted | extensibility | ||
| 0033 | Observability & operator telemetry — metrics, traces, log shipping Self-hosted ops teams cannot run a production service blind. The current Artist Alley setup ships /healthz and /readyz, structured JSON logs to stdout, and not much else. Operators need to: | Accepted | ops | ||
| 0032 | Audit log & change tracking — unified event log The audit-trail story has accreted across the roadmap. Phases 1.17, 1.20, 1.21, 1.24, 1.26, 1.27, 1.28, and 1.32 each carry an audit hook — table-level diffs, share-link fetches, sensitivity reveals, license events, bulk-op selection IDs, retention tombstones. There is even an… | Accepted | security | ||
| 0031 | Commerce — sell assets via Stripe + Shopify Operators running public-facing or semi-public Artist Alley instances want to sell their own content: digital downloads (concept-art packs, asset bundles, print-resolution files, retired game assets, soundtrack stems), print-on-demand merchandise, physical originals from archi… | Accepted | monetization | ||
| 0030 | Operator-configurable ad slots — feed, sidebar, footer Some operators will host Artist Alley as a public-facing community surface (fan sites, festival hubs, indie collectives, art-school portfolios) where ad-supported hosting is a sensible cost model. Other operators (AAA studios) will never run ads internally. Both should be serv… | Accepted | monetization | ||
| 0029 | Announcements home widget — operator-authored news + system events An operator-authored news strip on the homepage is a common DAM surface. The audit (2026-05-30) initially marked this as low-priority. User locked it in 2026-05-30. | Accepted | ux | ||
| 0028 | PBR 3D viewer polish — IBL controls, material inspector Phase 1.18.B-10 shipped the native 3D viewer (glTF / GLB / OBJ / FBX / Marmoset .mview, camera presets, IBL lighting, turntable poster, wireframe / UV inspect). The internal Marmoset-viewer reference and internal studio review processes both want a deeper PBR inspection layer: re… | Accepted | ux | ||
| 0027 | Featured collections — nested curation, access-scoped homepage The current homepage and account dashboards show "recent uploads" / "unreviewed" / "team feed" as flat lists. Nested featured collection trees — "Concept Art > Characters > 2026 Pitches > Hero Shots" — are a standard front-page curation primitive, with each node having its own thumbnail + description + access scope. | Accepted
→ ADR 0065 | ux | ||
| 0026 | AI creative editing — in-paint, out-paint, variations The DALL-E erase + regenerate pattern is widely used for marketing asset prep (remove background, regenerate a logo placement, generate variations for A / B testing), concept-art ideation (mask part of a sketch and regenerate it), and reference-image cleanup. | Accepted | extensibility | ||
| 0025 | Brand workspace — design tokens + brand kits + guidelines portal Game studios with a marketing / brand org maintain brand guidelines — logos, color tokens, font stacks, voice rules, do / don't examples, usage permissions, license states. Today, this lives in PDFs / Figma / Confluence and goes stale within a sprint. | Accepted | architecture | ||
| 0024 | Privacy & consent management — cookie banner, DSAR, retention A studio running Artist Alley as a self-hosted internal tool has minimal end-user-facing privacy obligations — the operating-system account map covers most. But the moment any of these is true, real GDPR / CCPA / LGPD considerations land: | Accepted | security | ||
| 0023 | RSS / Atom feeds — pull-style content syndication A reviewer / coordinator wants to know when new content matching a saved search lands. Push notifications (email, Slack, in-app) cover *now-active* notifications, but RSS/Atom is the format that lets people: | Accepted | architecture | ||
| 0022 | Chat platform integrations — Slack first, abstracted Studios live in Slack (most), Microsoft Teams (some), and Discord (indies + community-facing studios). Notifications from Artist Alley need to flow into those channels — review-request announcements, upload notifications, approval state changes, sensitivity-tier transitions, s… | Accepted | extensibility | ||
| 0021 | External platform integrations — Vimeo, YouTube, Adobe CC, bidirectional Earlier framing dismissed platform integrations as "platform lock-in" and proposed federation as the substitute. That framing was wrong for the actual use case. Studios need to: | Accepted | extensibility | ||
| 0020 | Asset gating & NDA workflow — pre-release blur, scheduled actions Game studios constantly handle pre-announcement material that must NOT be visible outside a small approved audience until a marketing date. The patterns are universal: | Accepted | security | ||
| 0019 | Bulk operations — multi-select, batch edit, exports, contact sheets Game studios manage 10k–500k+ assets. The current Artist Alley post detail flow assumes you act on one post or one asset at a time. Existing DAM tooling ships deep bulk-operation surfaces: multi-select edit, batch tag, batch delete, multi-row metadata import via CSV, configurable CSV export of search results, and printable contact-sheet generation. | Accepted | ux | ||
| 0018 | Share links — signed, expiring resource + collection URLs Game studios collaborate with people who do not have accounts on their Artist Alley instance: contractors, freelancers, publishers, IP owners, QA partners, agencies, festival juries, press embargoes. The post-detail permission model (capability + team membership) does not solv… | Accepted | architecture | ||
| 0017 | Monetization model + technical license enforcement ADR 0016 establishes the legal license direction (AGPL + commercial dual-license). This ADR specifies the runtime monetization model: the tier shape, the .lic file format, the enforcement architecture, and what we explicitly are not doing. | Accepted | monetization | ||
| 0016 | License direction — toward AGPL + commercial dual-license ADR 0002 placed the project under BSD-3-Clause on the explicit premise that "we have no monetization intent for artist-alley itself." That premise no longer holds. | Accepted | licensing | ||
| 0014 | Frontend stack — SvelteKit, Tailwind, embedded in the Go binary Phases 1.1–1.11 built the Go backend with the legacy PHP as the temporary frontend (strangler-fig per ADR 0003). The plan was that the legacy backend would render against our Postgres + Go API until each feature got its own native UI. | Accepted | ux | ||
| 0013 | Caching strategy — in-process LRU + Postgres LISTEN/NOTIFY, no Redis At 2M+ assets per server, every hot read path needs to avoid hitting Postgres for unchanged data. Specific pain points the metadata work (ADR 0012) is about to make worse: | Accepted | infrastructure | ||
| 0012 | Metadata model — admin-extensible fields, audit history, federation-ready ADR 0011 ships assets.metadata jsonb as an extensibility safety valve and a asset_tag join table. Neither is enough on its own: | Accepted | architecture | ||
| 0011 | Asset entity — UUID-keyed, clean-room design artist-alley's storage layer (ADR 0008, implemented in Phase 1.4) sits below the user-facing entity. storage_objects deduplicates byte streams by sha256; storage_variants records renditions; storage_pins reference-counts ownership. None of these are the *thing a user uploads,… | Accepted | architecture | ||
| 0010 | Permissions, teams, and workflow states The authorization model laid down in migration 00002_capabilities_roles.sql (Phase 1.3) gave us three of the seven layers a real production permissions system needs: | Accepted | architecture | ||
| 0009 | Collection model — three-axis primitives, query-or-manual membership, per-row TTL The prior generation of DAM tooling presents users with at least six visible collection "types" — Personal, Public, Featured, Smart, Request, Upload — plus a parallel structure for "featured categories" (a curator-maintained tree), a separate table for smart collections that pretend to be real ones in the UI, and external access keys grafted on as a sharing mechanism. | Accepted | architecture | ||
| 0008 | Storage architecture — content-addressed, pluggable backends artist-alley needs to store and serve game-art binaries: still images, multi-gigabyte videos, 3D models, audio, document previews, and any derived renditions (thumbnails, HLS segments, decimated 3D LODs). The Phase 1.4 work depends on getting this layer right; every later feat… | Accepted | infrastructure | ||
| 0006 | Go as the target backend; no sidecars ADR 0003 proposed a Strangler Fig pattern with multiple Go *sidecar* services (ai-gateway, review-sessions, video-pipeline, embeddings) talking to a forever- PHP backbone over HTTP. The PHP-side glue was to be a single plugin. | Accepted | architecture | ||
| 0055 | pg_search / ParadeDB as a future ranking-engine option (record-only research snapshot) Record-only ADR documenting current state of ParadeDB / pg_search as a potential BM25 upgrade over Postgres native ts_rank_cd, backed by 2026-07 web research. NOT a commitment — captures maturity, licensing, managed-provider support, and benchmark evidence so a future revisit has facts, not stale assumptions. | Proposed | architecture | — | |
| 0050 | Artist Alley as a Model Context Protocol (MCP) server Expose the instance's asset catalogue via the Model Context Protocol so AI coding agents (Claude Code, Cursor, Codex CLI, etc.) and creative agents can query and reason over a studio's archive the same way they query a codebase. | Proposed | extensibility | ||
| 0007 | Federation — thinking ahead Historical. Early "we will federate eventually, leave room for it in the schema" placeholder. Superseded by ADR 0043, which commits to the concrete walled-garden ActivityPub-shaped protocol. | Superseded
→ ADR 0043 | infrastructure | — | |
| 0005 | Postgres-only, no MySQL Phase 0 brought up two databases: MySQL (for the legacy backend) and Postgres + pgvector (for new-feature data). The original rationale was that the legacy backend is mysqli-only and migrating to Postgres would be a "3-6 month multi-thousand-call-site rewrite." | Superseded
→ ADR 0006 | infrastructure | ||
| 0004 | Adjunct Postgres data model ADR 0003 established that we run a Postgres + pgvector adjunct database for new-feature data, linked to the legacy backend by rs_resource_id / rs_user_id. Phase 0 brings up the Postgres container but the database is empty. | Superseded
→ ADR 0005 | infrastructure | — | |
| 0003 | Strangler Fig pattern applied internally Historical. The strangler-fig pattern proposed an incremental port from the bootstrapped upstream codebase to the new Go runtime, route by route. The approach was superseded by a clean-room rewrite in 2026-06; upstream code no longer ships. | Superseded
→ ADR 0006 | architecture | — | |
| 0002 | BSD-3-Clause license (initial choice) Historical. The project's initial license choice. Superseded by ADR 0016 (relicense to AGPL + commercial, executed 2026-07-11 (1.55.AA) — see ADR 0016). | Superseded
→ ADR 0016 | licensing | — | |
| 0001 | Hard fork from upstream trunk (historical) Historical. artist-alley was originally bootstrapped as a hard fork of an upstream open-source DAM in May 2026 to reuse permissions, resource types, metadata fields, preview pipelines, and plugin architecture rather than build the substrate from scratch. Superseded by ADR 0040 (clean-room reverse-engineering methodology) once the physical reference tree was deleted in Phase 1.55.S. | Superseded
→ ADR 0040 | process | — | |
| 0015 | PHP as legacy backend — historical transitional layer Historical. During the strangler-fig phase, the legacy PHP runtime served a shrinking set of routes via JSON wrappers. The arrangement was retired with the clean-room rewrite in 2026-06; the Go runtime now owns every route. | Deprecated | architecture | — |
Contributing a new ADR
Section titled “Contributing a new ADR”- Copy an existing ADR file in
docs/adr/as a template and claim the next four-digit number. - Fill in the frontmatter schema
—
id,title,status,date,area, andexcerptare required. - Follow the section structure: Context → Decision → Consequences → Alternatives considered, plus optional Implementation and References.
- Reference roadmap phases by their exact ID (e.g.
1.42,1.18.B-3) and related ADRs by their four-digit ID. The site build validates every cross-reference and fails on a typo. - The docs site picks up the new ADR automatically on next build via
site/scripts/sync-adrs.mjs.