feat(branding): resolve branding per tenant over the system theme (#373) [stack 11/11] - #395
Merged
Merged
Conversation
- Tenants override app_name, primary_color, design_pack, footer_text and
the three images as tenant_overridable settings keys (TENANT scope);
banner and footer links stay platform-only. Every tenant-scope write is
checked: same validators as the system value (422), and an image id must
be a live file owned by the tenant being written (404).
- branding.tenant_branding resolves per request: system object as is when
multi_tenant is off or no tenant is bound (no lookup), else the tenant's
overrides merged on top, in a tenant-keyed 30s TTL cache that forgets on
settings.values invalidation notices (all tenants on a system change).
- Shared props provider is async and leaves the tenant's theme on
request.state.branding for the pre-hydration <head>; the framework now
awaits async shared-prop providers.
- Anonymous asset routes resolve the tenant (subdomain or active org) and
serve a tenant-set image only as that tenant's own file, system values
as platform files; tenant images are Cache-Control private and immutable
only when ?v= names the file served.
- POST/DELETE /api/branding/tenant/{logo,logo-dark,favicon} upload tenant-
owned files (no platform=True) and reap the replaced one in the tenant's
scope; the organisation settings page offers uploads for file-id keys
(SettingDefinition.upload_url).
Claude-Session: https://claude.ai/code/session_01F8RiTBUJQnZmSq56qReZeV
Deploying simple-module-python with
|
| Latest commit: |
7d4731b
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://ec6d1658.simple-module-python.pages.dev |
| Branch Preview URL: | https://tenancy-11-branding.simple-module-python.pages.dev |
# Conflicts: # docs/modules/file_storage.md
…review of #373) (a)+(b) Image keys are refused (422) on every generic settings route; images are set/cleared only via /api/branding/tenant/{asset}, which validates the bytes. The asset route also refuses to serve a file outside the image allow-list (hand-edited rows). (b) Replaced images (tenant and system) are reaped after commit via register_on_commit (branding.reaper), in their own session, and only when no other image field of the same owner references them - a rollback no longer strands the setting on deleted bytes. (c) Asset Cache-Control: public+immutable only for a tenant-less request whose ?v= names the served file; tenant requests are private (Vary on Cookie and the tenant header); stale/missing ?v= is private, no-cache. (d) The shared-props provider skips /api/, /static/ and non-HTML requests; per-tenant single-flight for concurrent cache misses, and a forget() detaches in-flight reads. (e) Definitions send description_key (branding.tenant_settings.*), which TenantSettingRow translates with the English description as fallback. Claude-Session: https://claude.ai/code/session_01F8RiTBUJQnZmSq56qReZeV
# Conflicts: # modules/settings/settings/contracts/registry.py
…che, system saves publish, image keys clear_via (ship review) - a tenant logo without a dark logo no longer shows the platform dark logo - an empty-string override inherits the platform value (clear = delete) - the tenant cache, in-flight reads and epoch live on app.state.branding - the system theme save passes the invalidation bus so other workers forget - image keys declare clear_via so settings' generic deletes refuse them Claude-Session: https://claude.ai/code/session_01F8RiTBUJQnZmSq56qReZeV
# Conflicts: # modules/tenants/tenants/components/TenantSettingRow.tsx
…gone (qa BUG-005) Claude-Session: https://claude.ai/code/session_01F8RiTBUJQnZmSq56qReZeV
…pe guard (ship review r2) Claude-Session: https://claude.ai/code/session_01F8RiTBUJQnZmSq56qReZeV
# Conflicts: # modules/settings/settings/contracts/registry.py
…system route (ship qa r2) Claude-Session: https://claude.ai/code/session_01F8RiTBUJQnZmSq56qReZeV
antosubash
added this pull request to stack #397
October 1, 2026 16:14
antosubash
marked this pull request as ready for review
October 1, 2026 16:17
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #373. Stack 11/11, the top of the tenancy-adoption series (base: #394).
What
app_name,primary_color,design_pack,footer_text, the logo, the dark logo and the favicon. Writes go through the same validators as system branding.tenant_branding.py):multi_tenantoff, or no tenant on the request, it returns the system theme as before, with no lookup.settings.valuesnotices, with an epoch guard against races.async.branding_headprefers the per-request theme, so the<title>and favicon match before hydration.request.state.tenant_id(the subdomain resolver), otherwise the system.tenant_context, and a system value with platform files.Cache-Control: private, so a shared cache can't serve one tenant's logo to another./api/branding/tenant/{logo,logo-dark,favicon}, guarded bysettings.tenant.edit, for the active tenant only. The files are tenant-owned, and the old file is cleaned up on replace. The organisation settings page shows an upload control for these keys.Tests
multi_tenantoff is unchangedbranding_head.make test-py: 3470 passed.make test-js: 466 passed.make lintandmake doctorare clean.Follow-ups
/api/branding/tenant/*, not through the generic settings route.https://claude.ai/code/session_01F8RiTBUJQnZmSq56qReZeV