31 KiB
Multiple Platforms, Servers and Instances: White-Label Guide
How to set up and maintain a branded platform instance (Timebank.cc, Uuro or any other) running this
application. One instance runs per server; every instance runs the same code from git and differs only in
its .env, its platform folder platform/ (config, theme, logos, favicons, text and template overrides; outside
git) and its database content.
The platform folder is the recommended place for everything platform-specific. The older way, gitignored files in
config/ and uploaded branding files in storage/, keeps working; the platform folder wins where both exist.
| Instance | Server | Platform files (2026-10-19) |
|---|---|---|
| Timebank.cc | production | the older way: config/timebank_cc.php, config/themes.php; its logos and favicons are the defaults in the code. Moving it into platform/ is planned (php artisan platform:init --from-current). |
| Uuro | staging | its own branch uuro until its Laravel 11 upgrade; then everything in platform/ (runbook: RUNBOOK_LARAVEL_10_TO_11.md). |
Last checked against the code: 2026-10-19 (branch laravel-11, cd982cdd).
Contents
- Checklist for a new platform instance
- What makes an instance different
- The platform folder
- Platform configuration
- Theme
- Logos and favicons
- Platform name in texts and translations
- Platform-specific pages and switches
- Footer
- Custom content pages
- Keeping an instance up to date
- Troubleshooting
- Quick reference
- Related documentation
1. Checklist for a new platform instance
The server itself (PHP, MySQL, Redis, Elasticsearch, Reverb, mail) is set up with SETUP_GUIDE.md and PRODUCTION_DEPLOYMENT.md. Then, for the platform:
-
.env: set at leastAPP_NAME,APP_URLTIMEBANK_CONFIG=<platform>: the platform's name, e.g.myorg.timebank_config()reads the platform config under this name; see section 4.TIMEBANK_THEME=<theme>:timebank_cc,uuro,vegetableoryellow; not needed when the platform folder has atheme.phpof its ownDEPLOY_ENVIRONMENT=server,DEPLOY_APP_DIR,DEPLOY_SERVER_TYPE,DEPLOY_WS_URLDB_DEPLOY_USERNAME/DB_DEPLOY_PASSWORD: a MySQL user that may alter and restore the databaseMAIL_*,MAIL_SUPPORT_ADDRESS,MAIL_BOUNCE_ADDRESS
-
Platform folder (section 3), in the application root:
php artisan platform:init # platform/ from platform.example/ cp config/timebank-default.php.example platform/config.php # the platform config, from the template -
Platform config
platform/config.php: names, currency and terms per language, mail addresses, account limits, the switches in section 8, and the footer. The template usesExample Timebankand@example.orgas placeholders: replace all of them. -
Theme: pick a built-in one with
TIMEBANK_THEME, or make your own inplatform/theme.php(section 5). -
Branding files in
platform/public/:mail-logo.png,mail-logo-inverted.png, the favicons andog-image.png(section 6); a logo of your own inplatform/views/logos/<theme>.blade.php. Without them, emails get a generated text logo with the platform name, and the site uses the default favicons. -
First deploy:
./deploy.sh -e server. It creates the config files the code still needs from their templates (config/themes.phpand the two template configs; notconfig/<platform>.phpwhenplatform/config.phpexists), merges new keys intoplatform/config.phpand linksplatform/publicat/platform. -
Privacy policy: put your policy in
references/gdpr/<platform>/<YYYY-MM-DD>/privacy-policy-FULL-<locale>.md(the download link on the privacy page reads the newest dated folder). Without it the download returns 404. -
Content: static pages (FAQ, principles, getting started and so on) are posts in the database; create them in the admin area (section 10).
-
Background services: queue workers with all queues, Reverb and the scheduler (QUEUE_WORKERS_SETUP.md, WEBSOCKET_SETUP.md), search (ELASTICSEARCH_SETUP.md), bounce handling (BOUNCE_SETUP.md).
-
Check: open the site with your theme, log in, switch profiles, send a test mail (EMAIL-TESTING-GUIDE.md), and look for the old brand: no "Timebank.cc", no
example.orgaddresses, no city pages you do not want.php artisan platform:checkreports which config is used and whether/platformis linked. -
Off-site copy: put
platform/in a private git repository of its own (no secrets in it: those stay in.env); the server's backups stay on the server.
2. What makes an instance different
| Layer | In the platform folder (recommended) | The older way (still works) | In git |
|---|---|---|---|
| Environment, passwords | .env |
.env |
no |
| Platform behaviour, names, mail, limits, footer, switches | platform/config.php |
config/<TIMEBANK_CONFIG>.php |
no; templates config/*.php.example are |
| Theme colours, fonts, logo references | platform/theme.php |
an entry in config/themes.php, chosen with TIMEBANK_THEME |
no; template config/themes.php.example is |
| Website logo (SVG) | platform/views/logos/<theme>.blade.php |
resources/views/logos/<theme>.blade.php |
the older way is |
| Email/PDF logos, favicons, preview image | platform/public/ |
storage/app/public/app-images/, public/<favicon_dir>, public/<og_image> |
no |
| Texts and templates | platform/lang/, platform/views/ (overrides) |
none: shared by all instances | no |
| Static page content, posts, categories | database | database | no |
| Privacy policy | references/gdpr/<platform>/ |
the same | yes |
Everything else, the application code and the translations, is shared by all instances; a platform changes texts and templates through overrides in its platform folder, never in the shared files.
Config protection. The platform folder and the active config files are gitignored, so a git pull never
overwrites them.
deploy.sh creates a missing one from its .example template, and php artisan config:merge copies keys
that are new in the template into your file without changing your values. Details:
CONFIG_MANAGEMENT.md.
3. The platform folder
Everything that makes an installation one platform can live in one folder, platform/ in the project root
(PLATFORM_PATH in .env moves it). It is outside git (.gitignore), so deploys, upgrades and rollbacks never
touch it, and the same code from main runs on every server. Every part is optional: without the folder, or
without one of its parts, the installation runs on config/ and the files in the code as described in the
sections below. Loaded by App\Providers\PlatformServiceProvider.
| Path | What | Behaviour |
|---|---|---|
config.php |
The platform config | Replaces config/<TIMEBANK_CONFIG>.php; config:merge adds new keys to it |
theme.php |
One theme, with its id |
Becomes the active theme (instead of TIMEBANK_THEME) |
lang/<locale>.json, lang/<locale>/<group>.php, lang/vendor/<package>/... |
Changed texts, only the keys you change | Merged key by key over the code's texts; placeholders keep working |
views/..., views/mail/..., views/vendor/<package>/... |
Changed templates: pages, emails, the email layout, the logo, package views | Replace the whole template |
public/ |
Favicons, mail-logo.png, mail-logo-inverted.png |
Served at /platform/ after php artisan platform:link (deploy.sh runs it); used before the theme's files |
The tracked skeleton platform.example/ (with a README per path) is the starting point.
php artisan platform:init # create platform/ from platform.example/
php artisan platform:init --from-current # also copy config/<name>.php and the active theme into it
php artisan platform:override emails.layouts.html # copy a template to change it (also mail::html.header,
# wirechat::components.divider)
php artisan platform:link # serve platform/public at /platform
php artisan platform:check # read-only: which config is used, stale overrides
Texts or templates? 31 of the 34 email templates take their wording from translation strings, so a text override changes almost every email. Override a template only for another structure or layout.
Overrides go stale. A translation key is the English source sentence: when the code rewords it, an override
of the old sentence is no longer used. A copied template misses later fixes to its original.
php artisan platform:check reports both: keys the code no longer has (keys that the platform's own templates
use count as its own texts) and templates whose original changed since platform:override copied it (the first
line holds the original's fingerprint). Run it after every update.
Moving an existing installation into the folder. php artisan platform:init --from-current, copy the brand
files it lists into platform/public/, php artisan platform:link, check the site. config/<name>.php stays as a
fallback until you remove it; platform:check warns while both exist.
Good practice.
- No secrets in the folder: passwords and keys stay in
.env. Then the folder can be a private git repository of its own, which gives history and an off-site copy (the server backups stay on the server). The storage backup (scripts/backup-storage.sh) also archives it, inbackups/platform/. - The web server only reads the folder; normal file permissions are enough.
- After a change:
php artisan view:clearfor templates; restart the queue workers and Reverb after a config change (they read the config when they start). - Tests never read a developer's own platform folder (
PLATFORM_PATHinphpunit.xmlpoints elsewhere): they run onconfig/<TIMEBANK_CONFIG>.php. A developer whose config moved intoplatform/therefore keeps that file too (92 tests failed on the template alone, 2026-10-19: they depend on the developer's config values).
4. Platform configuration
File: platform/config.php; without it, config/<name>.php, where <name> is TIMEBANK_CONFIG in .env.
Either way the config is read under that name, so code uses timebank_config('key'). Any name works:
timebank_cc and timebank-default exist as templates, and a platform can use its own (uuro, myorg):
- Start
platform/config.phpfromconfig/timebank-default.php.example. Without a platform folder,deploy.shcreatesconfig/<name>.phpfrom that template when it is missing, and adds it to.git/info/excludeon that server, so deploys do not report it as an uncommitted change. - A development machine keeps
config/<name>.phpeven with a platform folder: the tests read that file and never the platform folder. php artisan config:merge <name>(and--all) adds new keys to the platform config (toplatform/config.phpwhen it exists) fromconfig/<name>.php.exampleif that exists, otherwise fromconfig/timebank-default.php.example. Lists (footer links, payment types, rules) are left as they are.
Do not copy the active config/timebank-default.php of another server: it may be outdated or hold that
server's settings. Start from the template.
Main sections (see the comments in the template for every key):
| Key | What |
|---|---|
platform_translations.<locale>.* |
platform_name, platform_name_legal, platform_name_short, platform_slogan, currency name, plural, symbol and position, thousand separator, platform_user(s), platform_principles, for each language (en, nl, de, es, fr) |
mail.* |
system_admin, content_admin, payments, chat_messenger, support addresses and names; platform_sign_off_signature per language |
accounts.<type> and profiles.<type> |
Account limits (limit_min, limit_max in minutes) and the transaction types an account may receive, per profile type |
permissions.<type> |
Which transaction types each profile type may make |
account_info.<type>.* |
Which balances and totals are shown publicly (*_public), to the owner (*_self) and to privileged viewers (*_privileged, see privileged_viewer_types) |
local_pages, android_app |
Platform-specific pages, see section 8 |
footer |
Footer sections and links, see section 9 |
seo |
allow_indexing_guest, allow_indexing_auth |
profile_inactive, profile_incomplete, delete_profile |
See PROFILE_INACTIVE_CONFIG.md, PROFILE_INCOMPLETE_CONFIG.md, PROFILE_AUTO_DELETE.md |
main_search_bar, search_optimization |
Search behaviour and boosts, see SEARCH_REFERENCE.md |
cert_monitor, health_monitor |
Certificate expiry warnings and the health check, mostly set through .env |
Reading values in code:
timebank_config('accounts.user.limit_max'); // the active platform's value
timebank_config('local_pages.the-hague', true); // with a default for a missing key
config('app.timebank_config'); // the platform config name itself
platform_name(); platform_currency_symbol(); // wording helpers, current locale
5. Theme
File: platform/theme.php, one theme with its own id, which becomes the active theme. Without it: the theme
named by TIMEBANK_THEME in config/themes.php (template config/themes.php.example).
Themes are applied at runtime through CSS variables, so one asset build serves every theme: switching
TIMEBANK_THEME needs no npm run build. Only VITE_* values in .env (such as the Reverb host) are
compiled into the build.
A theme of your own belongs in platform/theme.php: copy a whole theme block from config/themes.php.example
(every colour key is needed: a missing one leaves its CSS variable empty), give it an id, and change the name,
colours, typography and logos. php artisan platform:init --from-current writes the active theme there for you.
Its logo view may live in the platform folder too (platform/views/logos/mybrand.blade.php): nothing of the theme
needs to be in the code.
<?php
return [
'id' => 'mybrand',
'name' => 'My Brand',
'description' => '...',
'logos' => [
'svg_inline' => 'logos.mybrand', // platform/views/logos/mybrand.blade.php
'email_logo' => null, // platform/public/mail-logo.png is used first
'email_logo_inverted' => null, // platform/public/mail-logo-inverted.png
'favicon_dir' => null, // platform/public/ is used first
'og_image' => null, // platform/public/og-image.png is used first
],
'colors' => [ 'brand' => '#...', 'logo' => '#...', 'primary' => [50 => '#...', /* ... */ 900 => '#...'], /* ... */ ],
'typography' => [ 'font_family_body' => '...', 'font_family_heading' => '...', /* ... */ ],
];
The older way still works: the same block under a new key in config/themes.php, selected with TIMEBANK_THEME.
A new theme that should be available to every installation belongs in config/themes.php.example plus a
logo view in resources/views/logos/, committed to git.
In views and code, always use theme classes and helpers, never fixed colours: bg-theme-brand,
text-theme-primary, border-theme-border, theme_color('primary.500'), theme_font('font_family_body').
UI patterns: STYLE_GUIDE.md, THEME_CLASS_CONVERSION_GUIDE.md.
Technical details: THEME_IMPLEMENTATION.md.
6. Logos and favicons
Each asset is looked up in this order: the platform folder, the theme setting, the fallback.
| Asset | Platform folder | Theme setting (the older way) | Fallback when missing |
|---|---|---|---|
| Website logo (inline SVG) | platform/views/logos/<theme>.blade.php |
resources/views/logos/<theme>.blade.php (in git; see resources/views/logos/README.md) |
none: required for a theme |
| Email and PDF logo | platform/public/mail-logo.png |
storage/app/public/<logos.email_logo>, e.g. app-images/uuro_mail_logo.png |
a generated PNG with the platform name in the theme's logo colour; the timebank_cc theme falls back to app_mail_logo.png |
| Email logo for dark headers (newsletter) | platform/public/mail-logo-inverted.png |
storage/app/public/<logos.email_logo_inverted> |
the generated logo in white; timebank_cc: app_mail_logo_inverted.png |
Favicons: favicon.ico, favicon-32x32.png, favicon-16x16.png, apple-touch-icon.png |
platform/public/ |
public/<logos.favicon_dir>/ |
the files in public/ |
Optional icons: favicon.svg, favicon-96x96.png, site.webmanifest |
platform/public/ |
public/<logos.favicon_dir>/ |
the file in public/, else not linked |
Preview image for shared links (og:image, about 1200×630) |
platform/public/og-image.png |
public/<logos.og_image> |
no preview image; the other Open Graph tags (title, site name, URL, locale) are always there |
Files in platform/public/ are served at /platform/... once php artisan platform:link has run (deploy.sh
runs it). The layouts include one partial for the title, the icons and the Open Graph tags:
resources/views/layouts/partials/brand-head.blade.php.
Email logo: a PNG of about 900×700 pixels with a transparent background works best (emails show it about
120 pixels wide). Put it in platform/public/mail-logo.png (or, the older way, upload it to
storage/app/public/app-images/ and name it in the theme). Code reads it
through theme_email_logo_url() (emails) and theme_email_logo_file() (PDFs), with true for the inverted logo; the generated fallback is
App\Services\GeneratedLogo, cached in storage/app/public/app-images/generated/.
Favicons: put the files in platform/public/ (or, the older way, in
storage/app/public/app-images/favicons/<theme>/ with 'favicon_dir' => 'storage/app-images/favicons/<theme>' in
the theme). Layouts read them through theme_favicon('favicon.ico') and, for the optional ones,
theme_favicon_optional(); the preview image through theme_og_image().
The platform folder, uploaded logos (*_mail_logo*.png) and favicons are gitignored, so they do not appear as
changes in git status and survive every deploy.
7. Platform name in texts and translations
Translations are in resources/lang/<locale>.json (and resources/lang/<locale>/*.php), shared by all
platforms. Strings that mention the platform use placeholders, replaced from platform_translations:
| Placeholder | Replaced by |
|---|---|
@PLATFORM_NAME@, @PLATFORM_NAME_SHORT@, @PLATFORM_NAME_LEGAL@, @PLATFORM_SLOGAN@ |
names |
@PLATFORM_USER@, @PLATFORM_USERS@, @PLATFORM_PRINCIPLES@ |
terms |
@PLATFORM_CURRENCY_NAME@, @PLATFORM_CURRENCY_NAME_PLURAL@, @PLATFORM_CURRENCY_SYMBOL@ |
currency |
In code, render such a string with trans_with_platform('Why @PLATFORM_NAME_SHORT@?'), not with __()
(which leaves the placeholder in place). Never write a brand name into a translation key or a view. The
wording helpers platform_name(), platform_users(), platform_currency_symbol() and so on return the
values directly. Translation workflow: references/translations/.
A platform that wants other wording overrides single texts in its platform folder (platform/lang/,
section 3) instead of changing the shared files.
8. Platform-specific pages and switches
Some pages only make sense for one platform. They are switched in the platform config:
'local_pages' => [ // static pages about Timebank.cc's local groups
'the-hague' => false,
'lekkernassuh' => false,
'amst-brus-lisb' => false,
],
'android_app' => [
'enabled' => false, // the Android app page and the app's update check (/app/version)
'apk_file' => 'app-{version}.apk', // in storage/app/public/apk/; {version} is the version name
],
A page that is off has no route (404), and footer links to it are left out automatically. The default
template has all of them off; the Timebank.cc template has them on. A key missing from an older config keeps
the old behaviour (on) until config:merge adds it.
For an Android app of your own, see ANDROID_APP_UPDATE_FLOW.md;
scripts/deploy-apk.sh uploads an APK to another server with APK_REMOTE_HOST=... APK_REMOTE_APP_DIR=....
9. Footer
The footer is built from footer in the platform config:
'footer' => [
'sections' => [
[
'title' => 'Who we are', // translation key
'order' => 1, // left to right
'visible' => true,
'links' => [
['route' => 'static-faq', 'title' => 'FAQ', 'order' => 1, 'visible' => true],
['url' => 'mailto:info@example.org', 'title' => 'Email us', 'order' => 2, 'visible' => true],
['route' => 'static-messenger', 'title' => 'Chat messenger', 'order' => 3, 'visible' => true,
'auth_required' => true], // only for logged-in users
],
],
],
'tagline' => 'Your time is currency', // translation key
],
routeis a route name fromroutes/web.php; a route that does not exist on this platform (e.g. a switched-off local page) is skipped instead of causing an error.urlis for external ormailto:links.titlevalues are translation keys: add them toresources/lang/*.jsonfor every language.
10. Custom content pages
Static pages show posts from the database: each page view (e.g. resources/views/static/faq.blade.php)
embeds @livewire('static-post', ['type' => 'SiteContents\Static\Faq', ...]), and admins write the content
as posts in that category. Limits per page are in posts.static.<page> of the platform config.
A new page (a code change, so for every platform):
- Category type in
database/seeders/ExtendCategoriesSeeder.php, e.g.SiteContents\Static\MyPage, and its names and slugs in all 5 languages indatabase/seeders/ExtendCategoryTranslationsSeeder.php. - A view
resources/views/static/my-page.blade.php, modelled on an existing static page. - A route in
routes/web.php, in the localized group:Route::view(LaravelLocalization::transRoute('routes.static.my-page'), 'static.my-page')->name('static-my-page');with the URL slug per language inresources/lang/<locale>/routes.php. - Optional
posts.static.my-pagelimits in both config templates, and a footer link. deploy.shruns both seeders; locally:php artisan db:seed --class=ExtendCategoriesSeederand--class=ExtendCategoryTranslationsSeeder.
To change seeded content on every installation (rename a category type or slug), add a step to
app/Console/Commands/DatabaseUpdate.php (php artisan database:update, run by deploy.sh).
11. Keeping an instance up to date
- Routine deploys:
./deploy.sh -e server. It pullsmain, offers to merge new config keys, installs packages, migrates, re-indexes search and builds assets. Answeryand thenyesper file to the config merge; it adds new keys and keeps your values (backups instorage/config-backups/). - Check for new settings at any time:
php artisan config:merge --all --dry-run. Lists (footer sections and links, payment types, validation rules) are the platform's own choice: a list the platform config has is never changed by a merge; a missing one is added whole. - Stale overrides:
php artisan platform:checkafter every update. - Major upgrades (e.g. Laravel 11 to 12) go through
scripts/upgrade.sh(backup, maintenance window with tests, rollback), never through a routine deploy:deploy.shstops a server deploy that would raise the Laravel major version. The Laravel 10 to 11 upgrade is described in UPGRADE_LARAVEL_10_TO_11.md; its runbook is Uuro's, the last installation on Laravel 10, which leaves its own branch withupgrade.sh --leave-branch.
After a merge, review new keys in your platform config: new switches default to the template's value (off in the default template), and names or addresses from the template are placeholders.
12. Troubleshooting
| Problem | Check |
|---|---|
| Theme not applied | platform/theme.php has an id and wins over TIMEBANK_THEME; without it, TIMEBANK_THEME matches a key in config/themes.php. The theme has all colour keys (compare with themes.php.example). |
| Wrong or default settings | php artisan platform:check shows which config is used (platform/config.php wins over config/<name>.php); without a platform folder, TIMEBANK_CONFIG matches the file name without .php. Run php artisan config:merge <name> --dry-run for missing keys. Restart the queue workers and Reverb after a config change. |
| A changed text or template does not show | php artisan platform:check: the code may have reworded the sentence (the override's key no longer exists) or changed the original template. After changing a template: php artisan view:clear. |
Logo, favicon or preview image from platform/public/ not used |
public/platform is not linked: php artisan platform:link. |
| Brand name of another platform in a text | Look for a literal name in the view or translation key and switch it to a placeholder with trans_with_platform() (section 7). Texts in posts are content: edit them in the admin area. |
| Email shows a text logo instead of yours | No platform/public/mail-logo.png (or it is not linked), and the file named in logos.email_logo is missing in storage/app/public/. |
| Footer link missing | visible is true, the route exists on this platform, the section is visible, and auth_required matches. |
| Footer link causes an error | Should not happen any more (unknown routes are skipped); check the url/route key names. |
| City or Android pages appear on another platform | local_pages / android_app are missing from its config: run config:merge, then set them to false. |
| Privacy policy download gives 404 | No references/gdpr/<TIMEBANK_CONFIG>/<date>/privacy-policy-FULL-<locale>.md. |
deploy.sh asks about uncommitted changes |
Run git status: files changed on the server. The platform folder, uploaded logos, favicons and your platform config are excluded automatically; anything else was edited by hand. |
config:merge reports not merged or Merged config is invalid |
The file was left unchanged; see CONFIG_MANAGEMENT.md. |
13. Quick reference
# .env
TIMEBANK_CONFIG=<platform> # config/<platform>.php
TIMEBANK_THEME=<theme> # a key in config/themes.php (platform/theme.php wins)
PLATFORM_PATH=platform # the platform folder (default)
# Commands
./deploy.sh -e server # deploy (creates missing configs, merges new keys)
php artisan config:merge --all --dry-run # new keys waiting
php artisan config:merge <name> # merge into one config
php artisan config:merge --restore # restore a config from its backup
php artisan database:update # data updates of seeded content
php artisan platform:init [--from-current] # create the platform folder
php artisan platform:override <view> # copy a template into it
php artisan platform:link # serve platform/public at /platform
php artisan platform:check # stale overrides, config in use
timebank_config('key.path', $default) // platform setting
config('app.timebank_config') // platform config name
platform_name() trans_with_platform('...@PLATFORM_NAME@...')
theme_color('brand') theme_font('font_family_body') theme_logo('svg_inline')
theme_email_logo_url() theme_email_logo_file() theme_favicon('favicon.ico') theme_og_image()
platform_path('lang') // inside the platform folder
14. Related documentation
Setting up a server and instance
- SETUP_GUIDE.md: complete Debian server setup (PHP, MySQL, Redis, Elasticsearch, web server)
- PRODUCTION_DEPLOYMENT.md: production deployment
- EXTERNAL_SERVICES_REQUIREMENTS.md: external services an instance needs
- QUEUE_WORKERS_SETUP.md: queue workers (all queues)
- WEBSOCKET_SETUP.md, WEBSOCKET_SEPARATE_DOMAIN.md: Reverb
- ELASTICSEARCH_SETUP.md: search
- SSL_CERTIFICATE_RENEWAL.md: certificates
- DEPLOYMENT_HTMLPURIFIER_FIX.md: HTMLPurifier cache directory
Configuration and updates
- CONFIG_MANAGEMENT.md: config templates,
config:merge, backups, platforms with their own config name - UPGRADE_LARAVEL_10_TO_11.md: the Laravel 10 to 11 upgrade; RUNBOOK_LARAVEL_10_TO_11.md: its checklist for the Uuro server
platform.example/README.md: what goes where in a platform folder- PROFILE_INACTIVE_CONFIG.md, PROFILE_INCOMPLETE_CONFIG.md, PROFILE_AUTO_DELETE.md, PROFILE_DELETION_GRACE_PERIOD.md: profile lifecycle settings
- SEARCH_REFERENCE.md, SEARCH_REFERENCE_SIMPLE.md: search settings and behaviour
Look and feel
- THEME_IMPLEMENTATION.md: how themes work
- STYLE_GUIDE.md: UI patterns for every view
- THEME_CLASS_CONVERSION_GUIDE.md: converting fixed colours to theme classes
resources/views/logos/README.md: website (SVG) logos
- EMAIL-TESTING-GUIDE.md, ARTISAN-EMAIL-COMMAND-SUMMARY.md: testing emails per theme
- BOUNCE_SETUP.md, MAIL_BOUNCE_SYSTEM.md: bounce handling
Platform-specific features
- ANDROID_APP_UPDATE_FLOW.md: Android app and its update check
references/gdpr/<platform>/: privacy policy per platformreferences/translations/: translation workflow
For developers and AI agents: AGENTS.md in the repository root.