Files
timebank-cc-public/references/MULTI_PLATFORM_INSTANCES.md

31 KiB
Raw Permalink Blame History

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

  1. Checklist for a new platform instance
  2. What makes an instance different
  3. The platform folder
  4. Platform configuration
  5. Theme
  6. Logos and favicons
  7. Platform name in texts and translations
  8. Platform-specific pages and switches
  9. Footer
  10. Custom content pages
  11. Keeping an instance up to date
  12. Troubleshooting
  13. Quick reference
  14. 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:

  1. .env: set at least

    • APP_NAME, APP_URL
    • TIMEBANK_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, vegetable or yellow; not needed when the platform folder has a theme.php of its own
    • DEPLOY_ENVIRONMENT=server, DEPLOY_APP_DIR, DEPLOY_SERVER_TYPE, DEPLOY_WS_URL
    • DB_DEPLOY_USERNAME / DB_DEPLOY_PASSWORD: a MySQL user that may alter and restore the database
    • MAIL_*, MAIL_SUPPORT_ADDRESS, MAIL_BOUNCE_ADDRESS
  2. 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
    
  3. 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 uses Example Timebank and @example.org as placeholders: replace all of them.

  4. Theme: pick a built-in one with TIMEBANK_THEME, or make your own in platform/theme.php (section 5).

  5. Branding files in platform/public/: mail-logo.png, mail-logo-inverted.png, the favicons and og-image.png (section 6); a logo of your own in platform/views/logos/<theme>.blade.php. Without them, emails get a generated text logo with the platform name, and the site uses the default favicons.

  6. First deploy: ./deploy.sh -e server. It creates the config files the code still needs from their templates (config/themes.php and the two template configs; not config/<platform>.php when platform/config.php exists), merges new keys into platform/config.php and links platform/public at /platform.

  7. 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.

  8. Content: static pages (FAQ, principles, getting started and so on) are posts in the database; create them in the admin area (section 10).

  9. 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).

  10. 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.org addresses, no city pages you do not want. php artisan platform:check reports which config is used and whether /platform is linked.

  11. 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, in backups/platform/.
  • The web server only reads the folder; normal file permissions are enough.
  • After a change: php artisan view:clear for 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_PATH in phpunit.xml points elsewhere): they run on config/<TIMEBANK_CONFIG>.php. A developer whose config moved into platform/ 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.php from config/timebank-default.php.example. Without a platform folder, deploy.sh creates config/<name>.php from that template when it is missing, and adds it to .git/info/exclude on that server, so deploys do not report it as an uncommitted change.
  • A development machine keeps config/<name>.php even 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 (to platform/config.php when it exists) from config/<name>.php.example if that exists, otherwise from config/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=....


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
],
  • route is a route name from routes/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.
  • url is for external or mailto: links.
  • title values are translation keys: add them to resources/lang/*.json for 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):

  1. Category type in database/seeders/ExtendCategoriesSeeder.php, e.g. SiteContents\Static\MyPage, and its names and slugs in all 5 languages in database/seeders/ExtendCategoryTranslationsSeeder.php.
  2. A view resources/views/static/my-page.blade.php, modelled on an existing static page.
  3. 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 in resources/lang/<locale>/routes.php.
  4. Optional posts.static.my-page limits in both config templates, and a footer link.
  5. deploy.sh runs both seeders; locally: php artisan db:seed --class=ExtendCategoriesSeeder and --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 pulls main, offers to merge new config keys, installs packages, migrates, re-indexes search and builds assets. Answer y and then yes per file to the config merge; it adds new keys and keeps your values (backups in storage/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:check after 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.sh stops 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 with upgrade.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

Setting up a server and instance

Configuration and updates

Look and feel

Mail

Platform-specific features

  • ANDROID_APP_UPDATE_FLOW.md: Android app and its update check
  • references/gdpr/<platform>/: privacy policy per platform
  • references/translations/: translation workflow

For developers and AI agents: AGENTS.md in the repository root.