Split hosted access architecture

This commit is contained in:
Meta Vens 2026-04-26 05:05:21 +01:00
commit 532c189fe2
6 changed files with 522 additions and 444 deletions

View file

@ -21,8 +21,11 @@ diagnostics good enough to catch when we are lying to ourselves.
- Treat `docs/implementation-plan.md` as the forward plan.
- Treat `docs/lean-optimizer-algorithm-map.md` as the source-grounded live
algorithm map.
- Treat `docs/hosted-auth-architecture.md` as a future architecture note for
hosted auth work, not as proof that auth machinery already exists.
- Treat `docs/gamecult-hosted-access-architecture.md` as the future shared
access architecture for GameCult-hosted experiments, not as proof that auth
machinery already exists here already.
- Treat `docs/repixelizer-hosted-access-profile.md` as the Repixelizer-specific
binding onto that future shared access architecture.
- Update `state/map.yaml` when project understanding changes.
- Add evidence after meaningful research, implementation, verification, or
rejected paths, but keep it distilled. Routine "I just did this" proof belongs
@ -34,7 +37,8 @@ diagnostics good enough to catch when we are lying to ourselves.
- Project root: `E:\Projects\repixelizer`
- Implementation plan: `E:\Projects\repixelizer\docs\implementation-plan.md`
- Algorithm map: `E:\Projects\repixelizer\docs\lean-optimizer-algorithm-map.md`
- Hosted auth architecture: `E:\Projects\repixelizer\docs\hosted-auth-architecture.md`
- Shared hosted access architecture: `E:\Projects\repixelizer\docs\gamecult-hosted-access-architecture.md`
- Repixelizer hosted access profile: `E:\Projects\repixelizer\docs\repixelizer-hosted-access-profile.md`
- Handoff summary: `E:\Projects\repixelizer\notes\fresh-workspace-handoff.md`
- State CLI: `E:\Projects\repixelizer\tools\repixelizer_state.py`
- Pre-compaction helper: `E:\Projects\repixelizer\tools\repixelizer_prepare_compaction.py`

View file

@ -0,0 +1,333 @@
# GameCult Hosted Access Architecture
## What this file is
This file is a future shared architecture note for GameCult-hosted experiments
that need the same basic promise:
- let people sign in with Discord and/or Patreon
- decide access from GameCult/community entitlements
- keep app runtimes tied to local sessions and local accounts, not provider ids
It is not a map of currently landed auth code in this repo.
## Problem
Repixelizer is probably not the last hosted experiment that wants this shape.
If every experiment re-implements:
- provider OAuth
- identity linking
- entitlement refresh
- session cookies
- queue/job ownership
- "Discord role OR Patreon tier" logic
then we will end up with five damp little auth pits that are all almost the
same and all annoying in slightly different ways.
The reusable move is to build one GameCult hosted-access layer and let each app
bind its own policy and runtime seams onto it.
## Design goals
- one local account/session model across hosted experiments
- provider login and linking owned by one shared access layer
- per-app capability policies instead of hard-coded one-off gate checks
- host runtimes consume signed local session claims, not raw provider tokens
- reusable enough as a shared package first, with room to graduate into a
shared service later if that pain actually materializes
## Non-goals
- becoming a general public identity provider
- building multi-tenant SaaS auth machinery
- inventing global billing analytics because we got bored
- forcing cross-app single sign-on on day one before a second app exists
## Core concepts
- `Account`
one GameCult-local person record independent of external providers
- `LinkedIdentity`
one external provider identity attached to exactly one local account
- `EntitlementSnapshot`
cached result from a provider or grant source saying whether some claim is
currently true
- `CapabilityGrant`
a reusable local grant such as `global_member`, `app_access`, or
`admin_access`, scoped either globally or to one app
- `AppAccessProfile`
the per-app binding that names capabilities, allowed provider sources, and
policy rules
- `Session`
signed local app/browser session; the browser gets this, not provider tokens
- `AuditEvent`
durable record for login, link, unlink, refresh, denial, and admin overrides
## System shape
```mermaid
flowchart TD
A["Browser / app web UI"] --> B["Hosted experiment web surface"]
B --> C["GameCult access layer"]
C --> D["Discord OAuth + guild role checks"]
C --> E["Patreon OAuth + campaign/tier checks"]
C --> F["Local account/session/grant store"]
C --> G["Signed access claims"]
G --> H["Hosted experiment runtime"]
```
The important split:
- the access layer owns provider OAuth, identity linking, entitlement refresh,
and signed local sessions
- the host experiment owns its actual workload, queue, editor, render path, or
whatever weird machine it is
Do not smear provider logic directly through every host runtime.
## Reusable data model
Suggested durable core:
- `accounts`
- `id`
- `created_at`
- `last_seen_at`
- `display_name`
- `primary_email` nullable
- `linked_identities`
- `id`
- `account_id`
- `provider`
- `provider_user_id`
- `username`
- `access_token_encrypted`
- `refresh_token_encrypted`
- `token_expires_at`
- `profile_json`
- unique on `(provider, provider_user_id)`
- `entitlement_snapshots`
- `id`
- `account_id`
- `provider`
- `scope`
- `evaluated_at`
- `is_allowed`
- `reason_code`
- `reason_detail`
- `raw_summary_json`
- `capability_grants`
- `id`
- `account_id`
- `scope_type` enum: `global`, `app`
- `scope_id` nullable app slug
- `capability`
- `source` enum: `manual`, `invite`, `migration`, `operator`
- `status`
- `expires_at` nullable
- `note`
- `sessions`
- `id`
- `account_id`
- `app_slug`
- `created_at`
- `last_seen_at`
- `expires_at`
- `claims_json`
- `access_revision`
- `audit_events`
- `id`
- `account_id` nullable
- `session_id` nullable
- `app_slug` nullable
- `event_type`
- `event_payload_json`
- `created_at`
Important invariants:
- one provider identity belongs to one local account only
- provider tokens stay server-side only
- session cookies represent local sessions, not provider trust directly
- app-specific capability checks read local claims, not Discord or Patreon on
every route
## Provider adapters
Shared provider/gate adapters should be boring and reusable:
- `DiscordRoleEntitlementProvider`
- login via Discord OAuth
- entitlement check via guild membership and role ids, ideally through the
GameCult bot token server-side
- `PatreonMembershipEntitlementProvider`
- login via Patreon OAuth
- entitlement check via campaign membership and tier ids
- `ManualGrantProvider`
- local override for staff, migrations, experiments, or emergency access
- `InviteGrantProvider`
- reusable invite-issued app or role grants where that pattern makes sense
This is the good kind of bureaucracy: one place to keep the provider sludge.
## Policy model
The reusable core should not hard-code "Repixelizer access". It should evaluate
capabilities against an app profile.
Suggested app-profile shape:
```text
AppAccessProfile {
appSlug: string
displayName: string
capabilities: string[]
policyRules: CapabilityRule[]
publicRoutes: string[]
explanationStrings: map[string]string
}
```
Example rule style:
```text
app_access = discord.allowed_role || patreon.allowed_tier || grant.global_member || grant.app_access
queue_submit = app_access
admin_access = grant.operator || grant.admin_access
```
This gives reuse in the right place:
- the shared layer knows how to link identities, refresh entitlements, and
evaluate capabilities
- each experiment only defines which capabilities it wants and what qualifies
## Auth and linking flow
The generic flow should be the same across hosted experiments:
1. user starts OAuth from the host app UI
2. shared access layer signs OAuth `state`
3. provider callback resolves the provider identity
4. shared layer finds or creates the local account
5. if the user was already signed in locally, the identity may link onto the
existing account instead of creating a new one
6. shared layer refreshes entitlements and capability claims
7. shared layer issues or updates the local session cookie
The browser should never be the custodian of provider tokens. It is a goblin
with JavaScript.
## App integration contract
Every hosted experiment should integrate through the same seams:
- define an `app_slug`
- define the app capability profile
- mark which routes are:
- public
- authenticated
- capability-gated
- attach `account_id`, `session_id`, and `access_revision` to owned resources
such as jobs, drafts, uploads, or queue entries
- require resource-owner checks on private read/update/delete paths
If an app has a queue or job system:
- queue entries belong to local account/session ids, not provider ids
- entitlement refresh should block new submissions after access loss
- active-job cancellation policy is app-specific, but the recommended first cut
is still "let the active job finish, block new work"
## Deployment modes
This is where we avoid building the cathedral first.
### Mode 1: Embedded shared package
Recommended first cut.
- one shared `gamecult_access` package/module
- each hosted experiment embeds it
- each app keeps its own runtime and route integration
- sessions may still be app-local
Why this is the right first cut:
- reusable without forcing a separate always-on dependency
- small blast radius
- easy to evolve while only one or two apps use it
### Mode 2: Same-host shared access service
Use this when multiple apps on the same server want one shared access store and
session issuer, but full centralization still feels premature.
### Mode 3: Dedicated GameCult access service
Only do this when multiple independently deployed apps genuinely need:
- shared cross-app sessions
- central grant administration
- one callback origin for all providers
- enough auth churn that duplicated embedded deployments become the bigger pain
Do not build this first because it sounds important.
## Configuration split
Reusable shared config should stop pretending each app needs its own provider
stack.
Suggested generic env surface:
- `GC_ACCESS_ENABLED=1`
- `GC_ACCESS_SESSION_SECRET=...`
- `GC_ACCESS_BASE_URL=https://access.gamecult.org` or host-app local equivalent
- `GC_ACCESS_DISCORD_CLIENT_ID=...`
- `GC_ACCESS_DISCORD_CLIENT_SECRET=...`
- `GC_ACCESS_DISCORD_BOT_TOKEN=...`
- `GC_ACCESS_DISCORD_GUILD_ID=...`
- `GC_ACCESS_PATREON_CLIENT_ID=...`
- `GC_ACCESS_PATREON_CLIENT_SECRET=...`
- `GC_ACCESS_PATREON_CAMPAIGN_ID=...`
- `GC_ACCESS_ENTITLEMENT_CACHE_TTL_SECONDS=900`
- `GC_ACCESS_PROVIDER_FAILURE_GRACE_SECONDS=3600`
Per-app binding should live in code or profile config, not in a hundred cloned
provider env names:
- app slug
- hostnames
- allowed Discord role ids
- allowed Patreon tier ids
- capability rules
- public queue-summary policy, if any
## Recommended extraction path
1. Keep the repixelizer-specific note as an app profile, not the whole world.
2. Extract the shared ideas into a reusable `gamecult_access` package or sibling
repo once the first real implementation starts.
3. Move provider OAuth, signed session handling, identity linking, entitlement
refresh, and capability evaluation into that shared layer.
4. Leave app-specific route gating and runtime ownership fields in the host app.
5. Only consider a dedicated shared access service after a second hosted
experiment proves the need.
## Audit Result
Yes, this can be made reusable.
The correct reusable seam is:
- shared provider/session/entitlement/capability machinery
- thin per-app access profiles and runtime bindings
The incorrect reusable seam is:
- cloning a Repixelizer-specific auth blob into every future experiment and
changing a few strings with a kitchen knife.

View file

@ -1,441 +1,6 @@
# Repixelizer Hosted Auth Architecture
This file moved.
## What this file is
Use:
This file is a future architecture note for hosted-demo authentication and
entitlement work.
It is not a map of code that already exists in the repo. Current live hosted
demo behavior is still just:
- hosted-demo runtime limits and UI flags in `src/repixelizer/gui.py`
- the local single-process queue in `src/repixelizer/gui.py`
- frontend consumption of `hostedDemo` runtime config in `frontend/src/app.ts`
If future auth work lands and this note drifts away from the code, trust the
code first and either update this document or cut the stale parts instead of
pretending the architecture already exists.
## Summary
The hosted Repixelizer demo should support both:
- Patreon-backed access for supporters who do not use Discord
- Discord role-backed access for supporters or community members who do
Access should be granted if **either** entitlement source says yes.
That means we do **not** build two separate protected apps, two separate queue paths, or two separate user models. We build one local account system with multiple linked identities and one access policy.
## Goals
- Let a user sign in with either Discord or Patreon
- Grant access if the user:
- has one of the allowed Discord roles in the GameCult server, or
- has an allowed Patreon entitlement
- Allow optional account linking so one human can attach both providers to one local account
- Keep queue ownership, job cancellation, and hosted limits tied to one local user/session model
- Avoid duplicated entitlement logic spread across unrelated login handlers
## Non-goals
- Turning Repixelizer into a general identity provider
- Requiring every Patreon supporter to join Discord
- Requiring every Discord member to have Patreon connected
- Complex billing state history or subscription analytics
- Multi-tenant auth machinery
## External providers
### Discord
Use Discord OAuth2 authorization code flow for login.
Minimum use:
- identify the user
Recommended access pattern:
- log the user in with Discord OAuth
- use the existing GameCult bot token server-side to inspect that user's guild membership and role ids
Why this path:
- role truth already lives in Discord
- the Patreon bot already manages supporter roles there
- role checks stay anchored to the same operational source of truth
- the web app does not need to ask the user for broader guild scopes unless we later decide it is necessary
If needed, we can still support the OAuth `guilds.members.read` path later. It is not the preferred first cut.
### Patreon
Use Patreon OAuth2 authorization code flow for login.
After login:
- fetch the logged-in user's identity and membership data
- inspect membership to the GameCult campaign
- inspect current entitled tiers or equivalent supporter status fields
This path exists specifically for the "I want access to Repixelizer and I do not care about Discord" crowd.
## Core model
### Local user
Repixelizer should have one internal user record that is independent of any provider.
Suggested shape:
- `users`
- `id`
- `created_at`
- `last_seen_at`
- `display_name`
- `primary_email` nullable
- `access_state` cached summary, optional
### Linked identities
Each local user may have zero or more linked external identities.
- `linked_identities`
- `id`
- `user_id`
- `provider` enum: `discord`, `patreon`
- `provider_user_id`
- `username`
- `access_token_encrypted`
- `refresh_token_encrypted`
- `token_expires_at`
- `profile_json`
- unique on `(provider, provider_user_id)`
Important invariant:
- one provider identity can belong to only one local user
### Entitlement snapshots
Do not make every page hit go out to Discord or Patreon. Cache the last evaluated entitlement state locally.
- `entitlement_snapshots`
- `id`
- `user_id`
- `provider`
- `evaluated_at`
- `is_allowed`
- `reason_code`
- `reason_detail`
- `raw_summary_json`
Examples:
- Discord provider says `is_allowed = true`, `reason_code = allowed_role`
- Patreon provider says `is_allowed = false`, `reason_code = no_active_membership`
### Sessions
Hosted GUI sessions should be local application sessions, not raw provider tokens sprayed into the browser.
- `sessions`
- `id`
- `user_id`
- `created_at`
- `last_seen_at`
- `expires_at`
- `access_granted`
- `access_reason`
Store the session id in an HttpOnly secure cookie.
## Access policy
This is the heart of it.
Authentication answers:
- who is this user
Entitlement answers:
- should this user get through the gate
Those are different questions and should stay different in code.
### Provider-level entitlement checks
Implement two providers:
- `DiscordRoleEntitlementProvider`
- `PatreonEntitlementProvider`
Each provider returns something like:
```text
EntitlementResult {
provider: "discord" | "patreon"
allowed: boolean
reason_code: string
reason_detail: string
checked_at: datetime
}
```
### Unified policy
Implement one access policy:
```text
access = discord_allowed || patreon_allowed
```
That is the only rule that should matter to the hosted demo.
Everything else is explanation and caching.
### Allowed sources
#### Discord allowed
Grant access if the user has at least one configured allowed role in the configured guild.
Examples:
- Patreon supporter role
- founder role
- moderator role
- special alpha-tester role
Keep the role list environment-configured, not hardcoded in some grimy branch.
#### Patreon allowed
Grant access if the user has at least one allowed Patreon entitlement.
Examples:
- any paid membership
- one or more specific tiers
Keep this configurable too.
## Login and linking flows
### Flow A: Login with Discord
1. User clicks `Log in with Discord`
2. Complete Discord OAuth
3. Find or create linked identity `(discord, discord_user_id)`
4. Attach to existing local user if already linked
5. Otherwise create a new local user and attach the identity
6. Evaluate entitlements
7. Create local session with access granted or denied
### Flow B: Login with Patreon
1. User clicks `Log in with Patreon`
2. Complete Patreon OAuth
3. Find or create linked identity `(patreon, patreon_user_id)`
4. Attach to existing local user if already linked
5. Otherwise create a new local user and attach the identity
6. Evaluate entitlements
7. Create local session with access granted or denied
### Flow C: Link second provider
Once logged in, the user may choose `Link Discord` or `Link Patreon`.
Rules:
- linking requires an authenticated local session
- linking a provider already attached to another local user is rejected
- after linking, re-run unified entitlement evaluation
This turns "Patreon-only supporter later joins Discord" and "Discord member later subscribes on Patreon" into boring account maintenance instead of support tickets from hell.
## Queue and job integration
The hosted queue should attach jobs to local sessions and local users, not directly to provider ids.
Suggested additions:
- `jobs.user_id`
- `jobs.session_id`
- `jobs.entitlement_checked_at`
Rules:
- only authenticated sessions with `access_granted = true` may enqueue jobs
- queue position and active job state are scoped to the local user session
- heartbeat and cancel behavior stay exactly as already designed
If a user loses entitlement mid-session:
- do not allow new jobs
- queued jobs may be canceled on next entitlement refresh
- active job policy can be either:
- let current job finish, block future jobs
- or cancel active job immediately
Recommended first cut:
- let current active job finish
- reject future submissions
- cancel queued-but-not-started jobs after entitlement loss
That is less spiteful and avoids turning transient API issues into ruined runs.
## Entitlement refresh strategy
Do not ask Discord and Patreon on every request like a lunatic.
Use a cached snapshot with refresh triggers.
Refresh on:
- successful login
- provider link
- explicit `refresh access` action
- first protected request after snapshot TTL expires
Recommended TTL:
- 10 to 15 minutes
### Discord refresh
Fetch guild membership and roles using the bot token and configured guild id.
If the user is not in the guild, the Discord entitlement is false.
### Patreon refresh
Refresh via Patreon API using the stored Patreon identity and tokens.
If the membership lookup fails because tokens are stale, attempt refresh.
If refresh fails, mark Patreon entitlement unknown or false depending on the failure mode.
Recommended behavior:
- provider outage should not instantly hard-lock already active users if a recent positive snapshot exists
- use a short grace window for transient provider failures
## Configuration
Suggested environment variables:
- `REPIXELIZER_AUTH_ENABLED=1`
- `REPIXELIZER_SESSION_SECRET=...`
- `REPIXELIZER_BASE_URL=https://repixelizer.gamecult.org`
- `REPIXELIZER_DISCORD_CLIENT_ID=...`
- `REPIXELIZER_DISCORD_CLIENT_SECRET=...`
- `REPIXELIZER_DISCORD_BOT_TOKEN=...`
- `REPIXELIZER_DISCORD_GUILD_ID=...`
- `REPIXELIZER_DISCORD_ALLOWED_ROLE_IDS=role1,role2,role3`
- `REPIXELIZER_PATREON_CLIENT_ID=...`
- `REPIXELIZER_PATREON_CLIENT_SECRET=...`
- `REPIXELIZER_PATREON_CAMPAIGN_ID=...`
- `REPIXELIZER_PATREON_ALLOWED_TIER_IDS=tier1,tier2`
- `REPIXELIZER_ENTITLEMENT_CACHE_TTL_SECONDS=900`
- `REPIXELIZER_PROVIDER_FAILURE_GRACE_SECONDS=3600`
## UI behavior
The hosted landing state should show:
- `Log in with Discord`
- `Log in with Patreon`
If access is denied after login:
- say why in plain English
- show what qualifies
- offer linking the other provider if the user is logged in locally
Examples:
- `Your Discord account is signed in, but you do not have one of the allowed GameCult roles.`
- `Your Patreon account is signed in, but no active Repixelizer-eligible membership was found.`
- `Already a supporter on the other platform? Link it here.`
Once allowed:
- the queue UI and hosted GUI behave normally
## Security notes
- Use authorization code flow for both providers
- Use `state` properly for both OAuth flows
- Store provider tokens server-side only
- Encrypt stored refresh/access tokens at rest
- Use secure, HttpOnly session cookies
- Keep local session invalidation separate from provider logout
- Audit log link/unlink, login, entitlement refresh, and submission denial events
## Recommended implementation order
### Phase 1: Local auth scaffolding
- add local user, linked identity, session, and entitlement snapshot models
- add session cookie handling
- add protected-session middleware/helpers
### Phase 2: Discord login and entitlement
- implement Discord OAuth login
- implement bot-backed guild role entitlement check
- gate the hosted queue behind Discord-based access
### Phase 3: Patreon login and entitlement
- implement Patreon OAuth login
- implement Patreon membership/tier entitlement check
- unify access policy across both providers
### Phase 4: Identity linking
- allow linking the second provider
- re-run entitlement evaluation on link/unlink
### Phase 5: Operational hardening
- add entitlement refresh TTLs and provider-failure grace behavior
- add UI for denied states and manual refresh
- add audit logs and admin diagnostics
## Invariants
Keep these sacred:
- login provider is not the same thing as entitlement provider
- the queue only knows local users and local sessions
- access is granted if either provider says yes
- provider-specific logic stays isolated behind entitlement providers
- linking identities must never merge two existing local users implicitly
If we violate those rules, the auth system will become a damp little pit of edge cases almost immediately.
## Audit Result
This design is still compatible with the current Epiphany workflow after one
important correction: it must be treated as future architecture, not current
truth.
That boundary is now explicit. The note can keep its job if it stays focused on:
- the proposed local-user plus linked-identity model
- the `discord_allowed || patreon_allowed` entitlement rule
- the implementation phases and invariants for future auth work
It should not quietly become:
- a handoff note
- a claim that auth is already landed
- a duplicate of the live queue behavior already described by the code
- `docs/gamecult-hosted-access-architecture.md` for the reusable shared design
- `docs/repixelizer-hosted-access-profile.md` for the Repixelizer-specific binding

View file

@ -0,0 +1,172 @@
# Repixelizer Hosted Access Profile
## What this file is
This file binds Repixelizer onto the shared future design in
`docs/gamecult-hosted-access-architecture.md`.
It is still future design, not a claim that the hosted demo already has auth.
## Current repo boundary
Current live hosted-demo behavior is still just:
- hosted-demo runtime limits and UI flags in `src/repixelizer/gui.py`
- the local single-process queue in `src/repixelizer/gui.py`
- frontend runtime-config consumption in `frontend/src/app.ts`
No Discord login, Patreon login, linked identities, local sessions, or access
gates are landed yet.
## App identity
- `app_slug`: `repixelizer`
- primary hostname: `repixelizer.gamecult.org`
- runtime shape: one hosted GUI plus one queue-backed worker loop inside the app
## Capabilities
Repixelizer does not need a baroque permission lattice.
The likely first-cut capabilities are:
- `app_access`
- may load the protected hosted GUI
- `queue_submit`
- may create a repixelizer job
- `job_read_own`
- may read own job state, event stream, and final output
- `job_cancel_own`
- may cancel own queued or running job
- `admin_access`
- may inspect operational or grant/admin surfaces later
For the first cut:
```text
queue_submit = app_access
job_read_own = app_access + resource ownership check
job_cancel_own = app_access + resource ownership check
```
## Access policy
Repixelizer should inherit the generic GameCult access pattern:
```text
app_access =
discord.allowed_role
|| patreon.allowed_tier
|| grant.global_member
|| grant.app_access
queue_submit = app_access
admin_access =
grant.operator
|| grant.admin_access
```
Likely Discord-allowed examples:
- Patreon supporter role
- founder role
- moderator role
- explicit alpha-tester role
Likely Patreon-allowed examples:
- any paid GameCult membership
- or one configured subset of tiers if the experiment needs a stricter gate
## Repixelizer runtime binding
When this lands, the shared access layer should integrate with the current queue
shape rather than replace it.
Suggested queue/job additions:
- `jobs.account_id`
- `jobs.session_id`
- `jobs.access_revision`
- `jobs.entitlement_checked_at`
Route policy:
- public:
- `/api/health`
- `/api/config`
- public or capability-gated by policy choice:
- `/api/queue`
- requires `queue_submit`:
- `POST /api/jobs`
- requires ownership:
- `GET /api/jobs/{job_id}`
- `GET /api/jobs/{job_id}/events`
- `POST /api/jobs/{job_id}/heartbeat`
- `DELETE /api/jobs/{job_id}`
Important invariant:
- job ownership must resolve from the local session/account, not from raw
Discord or Patreon identifiers
## UI binding
Once access exists, the hosted landing flow should become:
- anonymous visitor sees:
- `Log in with Discord`
- `Log in with Patreon`
- signed-in but denied visitor sees:
- plain-English denial reason
- what qualifies
- link-the-other-provider affordance
- allowed visitor sees:
- the normal hosted GUI and queue flow
The queue/progress UI itself should stay as close as possible to the current
runtime. The auth layer should gate entry, not rewrite the machine for fun.
## Config mapping
Repixelizer-specific auth config should not grow a separate provider jungle.
Keep:
- existing runtime/queue/limit env vars under `REPIXELIZER_*`
Add shared access config under `GC_ACCESS_*`.
Repixelizer-specific app-profile config can live under either:
- code constants, or
- a small profile/config surface such as:
- `GC_ACCESS_REPIXELIZER_ALLOWED_DISCORD_ROLE_IDS=...`
- `GC_ACCESS_REPIXELIZER_ALLOWED_PATREON_TIER_IDS=...`
- `GC_ACCESS_REPIXELIZER_PUBLIC_QUEUE_SUMMARY=1`
The exact encoding can be decided later. The important thing is not cloning the
provider credentials into a second app-specific namespace.
## Recommended implementation order
1. Extract or embed the shared `gamecult_access` layer described in
`docs/gamecult-hosted-access-architecture.md`.
2. Gate `POST /api/jobs` plus per-job read/cancel/event routes through local
sessions and ownership checks.
3. Add login/link/denied-state UI around the existing hosted GUI shell.
4. Decide whether `/api/queue` stays public summary or moves behind `app_access`.
5. Only after a second hosted experiment exists, decide whether the shared
access layer should leave embedded-package mode and become a shared service.
## Invariants
- Repixelizer should consume the shared access layer; it should not become the
template that future experiments copy by hand.
- The queue remains owned by local session/account ids.
- Provider tokens stay server-side only.
- Capability checks belong at route boundaries and resource ownership seams, not
scattered through queue internals.
- Auth should gate the hosted demo, not mutate the solver model or pretend to be
the product.

View file

@ -33,7 +33,9 @@ Do not trust this file for the exact live HEAD. Always check git.
- The current tracked weakness is the widened dark contour near the badge sword tip on tapered shapes.
- `source_structure` exists because lattice-only `source_fidelity` could call visibly better outputs worse.
- The old tray optimizer is dead and should stay dead unless the entire machine map changes for a real reason.
- `docs/hosted-auth-architecture.md` is a future design note for a not-yet-landed hosted auth system; current hosted-demo truth is still just the queue and limit machinery in `src/repixelizer/gui.py` plus the frontend runtime-config plumbing.
- `docs/gamecult-hosted-access-architecture.md` is the future shared design for reusable GameCult-hosted access across experiments.
- `docs/repixelizer-hosted-access-profile.md` is the Repixelizer-specific binding onto that future shared access layer.
- current hosted-demo truth is still just the queue and limit machinery in `src/repixelizer/gui.py` plus the frontend runtime-config plumbing.
## Critical Doctrine
@ -90,7 +92,8 @@ Rules now in force:
- this handoff is a compact re-entry packet.
- `docs/implementation-plan.md` is the forward plan.
- `docs/lean-optimizer-algorithm-map.md` is the source-grounded control-flow map.
- `docs/hosted-auth-architecture.md` is future auth design, not live implementation truth.
- `docs/gamecult-hosted-access-architecture.md` is future shared auth/access design, not live implementation truth.
- `docs/repixelizer-hosted-access-profile.md` is the app-specific future binding, also not live implementation truth.
Do not let any one note become all of those things. That is how the tower grows
sideways and starts calling itself architecture.

View file

@ -2,3 +2,4 @@
{"ts":"2026-04-26T00:00:00Z","type":"metric","status":"accepted","note":"source_structure is tracked alongside source_fidelity because lattice-only scoring could rank visibly better outputs as worse."}
{"ts":"2026-04-26T00:00:00Z","type":"regression","status":"rejected","note":"The d9fa411 phase-field tuning pass was a real regression on the pinned badge case; reverting it restores the known-good badge result exactly."}
{"ts": "2026-04-26T03:57:11+00:00", "type": "docs", "status": "ok", "note": "Audit pass clarified note boundaries: the algorithm map remains the live control-flow truth, the implementation plan remains the forward hypothesis ledger, and hosted-auth-architecture is explicitly future design rather than landed auth machinery."}
{"ts": "2026-04-26T04:05:06+00:00", "type": "design", "status": "accepted", "note": "Hosted auth was split into a reusable GameCult hosted-access architecture plus a thin Repixelizer-specific access profile. Recommended extraction path is an embedded shared gamecult_access layer first, with dedicated service only after a second app proves the need."}