Platform release history — what shipped, when, and where.
EMAIL_CHANNEL_MODE gate so it stays dormant until the email cutover flips the mode (the new template is not exempt). Delivery uses one generic template generic_admin_email rendering the bell title/body (no deep link in v1 — the email tells the admin to log in to review); the delivery report still shows the real event slug per email row. Both generic system templates (generic_in_app, generic_admin_email) are now hidden from the admin template listing
php artisan tenant:seed {slug} --class=AdminNotificationEmailTemplateSeeder (idempotent; or re-run DefaultEmailTemplateSeeder / OkmEmailTemplateSeeder, which now include it); 2) notification — deploy and restart the queue workers; 3) admin_ui — copy change only. Until the template is seeded on a tenant, the email leg silent-skips (no_active_template) and the bell still fires. Admin email does not actually deliver until EMAIL_CHANNEL_MODE is test or on — test with a redirect inbox first. See documents/ADMIN_EMAIL_NOTIFICATIONS_PLAN.md.
Mail:: sends remain on any microservice branch
system.email_brand doesn't match. Caveats: admin-customised bodies are reset to the stock version (as a new version — the custom one stays in history; old notifications keep their pinned versions), and any deactivated seeder-owned template is re-activated. Guards (notification): required_template_placeholders — template saves missing their secret placeholders ({{otp}}, {{main_password}}, {{invest_password}}, {{new_password}}, {{password}}, and retro-protected {{reset_url}}/{{verify_url}}) are rejected 422; secret_template_variables — secret values are masked (••••••) in the stored notifications row after delivery, so credentials never sit readable in the DB or admin log. Masked rows re-render masked (deliberate). email_exempt_slugs synced with the 2026-06-13 call-site audit: all 20 cut-over events plus the 10 secret slugs now bypass EMAIL_CHANNEL_MODE (they have no legacy fallback / must always deliver for real). Previously only 2 were listed — off/test mode would have silenced 18 events. Semantics preserved: OTP and invite-resend endpoints return 500 when the queue submission fails (user/admin retries), as legacy did. OTP delivery is now queued — verify worker latency (created_at → sent_at) after deploy. Dormant code deleted: profile's AccountController + RegisterController and account's RegisterController (unrouted dead copies with unreachable credential sends). See documents/BATCH9_SECRET_EMAILS_PLAN.md for the full plan + implementation deltas.
apikey.scope:{client|admin} middleware on all tenant routes — flutter/web keys are limited to client endpoints, admin keys to admin panel endpoints; follows VALIDATE_API_KEY_MODE (off = no-op, log = warn only, enforce = 403); keys without scopes behave as * so existing minted keys are unaffected. Dev tenant verified end-to-end under enforce: web/admin/flutter keys minted with scopes, both UIs working, flutter key 403-blocked on admin routes
X-API-Key on all gateway requests — the config entry existed but no service attached the header, so enforce mode would have broken the whole admin panel
documents/API-Gateway/openapi-client.yaml documents all 66 client-scope endpoints (Auth, Profile/KYC, Accounts/MT5, Finance, Tickets, Notifications, Public) with request fields sourced from the downstream microservices' validation rules; covers the two-layer auth model (X-API-Key + Bearer) and the success-flag response convention; lints clean (Redocly) and imports directly into Postman / Swagger UI — this is the Flutter developer handover document
GET /api-docs (ui_gateway) that merges the **functional guide and the API reference**: for each feature (auth, MT5 accounts, funding, deposits, withdrawals, transfers, profile/KYC, payout methods, promotions, notifications, tickets, public config) it shows the prose explanation and the rules, then the endpoints that power it (params, request bodies, responses, examples). Custom dependency-free renderer (no Swagger/Redoc), sidebar nav, downloadable spec (OpenAPI YAML + JSON), behind an HTML login form (DOCS_AUTH_USER / DOCS_AUTH_PASSWORD; 404 if unset). Prose source resources/docs/user-guide.md (rendered via Str::markdown); endpoints from the OpenAPI spec copy. Documents the payment-method gap (no dedicated endpoint — read payment_method from GET /get-account with ?direction=deposit|withdrawal). /user-guide redirects here
/api-docs page: 1. git pull origin ui_gateway 2. Set the docs login in .env (both required, or the page 404s): DOCS_AUTH_USER, DOCS_AUTH_PASSWORD. 3. php artisan route:clear && php artisan config:clear && php artisan view:clear 4. Reload the worker (systemctl reload php-fpm or restart the container) so the new controller/middleware classes load. Dependency: the page renders Markdown via Str::markdown (league/commonmark, ships with Laravel). If it errors on a missing CommonMark class, run composer require league/commonmark once on ui_gateway. No other composer changes. Verify: /api-docs prompts for the login, then renders feature sections (prose + endpoints) with a working sidebar; /user-guide redirects to /api-docs.
AuditApiRequest middleware logs one metadata-only JSON line per gateway request to a dedicated daily apilog channel in storage/logs/api-audit/ (request id, api key id + client type, tenant, method, path, status, ip, user agent, duration). Runs first in the tenant stack so rejected requests (401/403/429) are recorded too; echoes X-Request-Id for correlation. Never logs request/response bodies. Retention via API_AUDIT_RETENTION_DAYS (default 90)
Batch8RemainingEmailTemplateSeeder — security alerts (suspicious_login, admin_suspicious_login, admin_impersonation_alert), profile_updated, document_remarks_added, the 4 IB partner notifications (ib_new_referral, ib_referral_activated, ib_referral_stats_updated, ib_commission_paid), and the redesigned documents_required + compliance_alert
php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder (idempotent via SHA-256 content-hash dedup). documents_required and compliance_alert were rebuilt from one-line stubs to the full Default style — re-seeding creates a new version of each (old notifications keep their pinned version). Deferred: new_promotion_available / promotion_status_changed (P4 marketing broadcasts) — no template until the broadcast composer + opt-in system exist. Email template track complete. Remaining work is not authoring: OKM ports (3/~64), call-site wiring on the microservice branches, and the by-design Blade-only system-secret emails. No call-site wiring in this change.
ticket_created, ticket_replied, ticket_created_by_admin, admin_ticket_assigned, admin_ticket_comment, admin_email_verified, admin_role_updated, admin_account_removed) in Batch7SupportAdminEmailTemplateSeeder
php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder (idempotent via SHA-256 content-hash dedup). OKM tenants render these with the Default body until OKM ports are authored. admin_invite excluded — the employee-credentials email delivers a temporary password; stays on its Blade path, never admin-editable (same rule as the OTP / credential exclusions). Admin login OTPs (_otp / _whatsapp / _sms) are excluded too; admin_password_reset_link / _success already shipped in Batch 1. No call-site wiring — templates authored only. Five of these are admin-addressed (recipient_type='admin').
leverage_updated, mt_manual_deposit, mt_manual_withdrawal, mt_account_deleted, mt_account_unlinked, mt_account_transferred, mt_account_disabled, mt_account_request_approved, mt_account_request_rejected) in Batch6Mt5AdminEmailTemplateSeeder
php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder (idempotent via SHA-256 content-hash dedup). OKM tenants render these with the Default body until OKM ports are authored. mt5_password_changed_by_admin excluded — carries the new MT5 password; stays on its account_microservice Blade path, never admin-editable (same rule as the OTP and MT5-credential exclusions). No call-site wiring — templates authored only.
admin_created_account, account_blocked, account_unblocked, account_deactivated, account_deleted, password_reset_by_admin, wallet_credited_by_admin, wallet_debited_by_admin, internal_transfer_by_admin, profile_updated_by_admin, document_uploaded_by_admin) in Batch5AdminActionsEmailTemplateSeeder
php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder (idempotent via SHA-256 content-hash dedup). OKM tenants render these with the Default body until OKM ports are authored. Credential-free by design. admin_created_account uses an activation link and password_reset_by_admin uses a reset link — neither emails a temporary password, keeping them inside the secret-exclusion boundary (same principle as the OTP / MT5-credential exclusions). No call-site wiring — templates authored only.
withdrawal_submitted_crypto, internal_transfer_submitted, internal_transfer_success, internal_transfer_rejected, transfer_comment_added, promotion_claimed) in Batch4CryptoTransferEmailTemplateSeeder
php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder (idempotent via SHA-256 content-hash dedup). OKM tenants render these with the Default body until OKM ports are authored. Credential-bearing account emails excluded. live_account_created, demo_account_created, and mt5_password_changed carry system-generated MT5 passwords and are NOT templates — they stay on their account_microservice Blade paths, never admin-editable (same rule as the OTP emails). No call-site wiring — templates authored only. Added a gift icon variant to BuildsEmailTemplates for promotion_claimed.
bank_account_added, bank_account_updated, crypto_address_added, crypto_address_updated, crypto_address_deleted, kyc_approved, kyc_rejected, document_status_changed, document_uploaded) in Batch3KycPayoutEmailTemplateSeeder; shared body builders extracted into a BuildsEmailTemplates trait
php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder (idempotent via SHA-256 content-hash dedup). OKM tenants render these with the Default body until OKM ports are authored. Document-upload confirmations are one generic template. document_uploaded takes a {{document_type}} variable instead of four per-type slugs (document_uploaded_identity / _address / _networth / _signature). Call sites send document_uploaded + a type string. Payout-destination changes (bank/crypto add/update/delete) are P1 security alerts — each carries a "Didn't do this?" callout. Excluded: bank/crypto method add-OTPs (system OTP, Blade path); admin-initiated doc/profile events (Batch 5). bank_account_deleted was already shipped and is unchanged. Refactor: the shared shell/icon/heading/detailPanel/callout/button/greeting builders moved into BuildsEmailTemplates; Batch 2 was refactored to use it (rendered output unchanged). No call-site wiring — templates authored only.
withdrawal_submitted, withdrawal_approved, withdrawal_rejected, withdrawal_comment_added, deposit_submitted, deposit_approved, deposit_rejected, deposit_comment_added, deposit_received_psp) in Batch2MoneyEmailTemplateSeeder, aggregated by DefaultEmailTemplateSeeder
php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder (idempotent via SHA-256 content-hash dedup). OKM tenants render these with the Default body until OKM ports are authored. withdrawal_confirmed removed — it was a test-only stub. withdrawal_approved is the real withdrawal-approval template. The row persists on already-seeded dev tenants (template hard-delete is FK-restricted); deactivate or delete it manually there if desired — no call site sends it. withdrawal_otp excluded (system-generated OTP, stays on its finance Blade path). Crypto withdrawal + internal transfers deferred to Batch 4. No call-site wiring in this change — templates authored only; send call sites remain to be wired on the microservice branches.
password_changed, demo_email_verification, demo_welcome, admin_password_reset_link, admin_password_reset_success) authored in a self-contained Batch1AuthEmailTemplateSeeder, aggregated by DefaultEmailTemplateSeeder
php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder. Idempotent — re-running adds only the new slugs; unchanged templates keep their existing version via SHA-256 content-hash dedup, so no spurious versions are created. OKM-branded tenants also run --class=OkmEmailTemplateSeeder; the 5 new slugs render with the Default-styled body until OKM ports are authored. admin_login_otp is intentionally NOT a template. The login OTP is generated and validated server-side, so its email body must not be admin-editable. It stays on its legacy Blade path on admin_backend (emails/otp.blade.php). No call-site wiring in this change — these templates are authored only. The events that send them remain to be wired on their respective microservice branches (separate track). Seeder structure going forward: each future batch (2-8) ships as its own provider class merged by DefaultEmailTemplateSeeder. The four already-shipped auth templates stay defined in DefaultEmailTemplateSeeder to keep their content hash stable.
api_keys table (hashed SHA-256), three-mode middleware (off / log / enforce) on api_gateway, and CLI tooling for minting/listing/revoking keys
php artisan tenant:migrate {slug} for every active tenant. Adds api_keys table with hashed key storage, client_type, scopes, expiry, revocation, and last-used tracking. Default mode is off — this release is operationally a no-op until the env flag is flipped. Set VALIDATE_API_KEY_MODE=off in api_gateway .env (or leave unset) to keep current behaviour. The middleware is attached to all tenant routes but short-circuits when mode is off. Rollout sequence per tenant (see OPERATIONS_RUNBOOK.md §12 for full commands): 1. Mint three keys per tenant via php artisan apikey:create {slug} --type={web|admin|flutter} --name="..." on migration-service. Each key's plaintext is shown once. 2. Set API_GATEWAY_KEY on each tenant's ui_gateway, admin_ui, and Flutter build to the matching key. config:clear after. 3. Flip api_gateway to VALIDATE_API_KEY_MODE=log and watch logs for apikey.missing / apikey.invalid for ~3-7 days. 4. Once clean, flip to VALIDATE_API_KEY_MODE=enforce. Exempt routes (no key required): POST /{tenant}/webhook/match2pay, /{tenant}/ibp/*, /up. Rollback is VALIDATE_API_KEY_MODE=off + config:clear on api_gateway. No code redeploy needed. Existing X-API-Key header is now real. ui_gateway and admin_ui have always sent X-API-Key: {API_GATEWAY_KEY} but it was unenforced. Once mode is flipped to enforce, the env value must match a real key in the tenant's api_keys table or every request returns 401.
?ref= into a 60-day encrypted cookie, register-submit verifies the code through the existing IBP outbound channel (3s timeout, no-op when bridge unconfigured), and the new user row is stamped with referred_by_id + referral_code_of_ib. The existing pushUserRegistered IBP push now carries the resolved (referral_id, referral_code) pair so IBP can link the user to the IB's wallet. Verify failures surface in a new admin "IBP Logs" page (logs.ibp.view permission) and trigger a rate-limited alert email (configurable via IBP_FAILURE_ALERT_EMAIL, default ataria@4xportal.com, max one per tenant per 5 min). Demo register captures the cookie but does not attribute the signup.
Tenant DB migration required. Run php artisan tenant:migrate {slug} for every active tenant. Adds referred_by_id and referral_code_of_ib to users, plus the logs.ibp.view permission key. IBP_FAILURE_ALERT_EMAIL env optional on admin_backend. Defaults to ataria@4xportal.com if not set. Override via env to redirect alerts elsewhere. config:clear after change. Bridge must be configured per tenant for any of this to fire. With no active row in ibp_bridges, the verify call short-circuits with skipped: true, attribution stays null, and no IBP HTTP calls are made (same guard as the existing pushUserRegistered path). Configure via IbpBridgeSeeder per documents/API-Gateway/ibp-bridge-setup.md.
email:test-batch artisan command — fires every active email template with auto-generated sample variables through the real dispatch path, for Phase 1 validation and regression re-checks
documents/PHASE1_VALIDATION_CHECKLIST.md
EMAIL_CHANNEL_MODE gate (off/test/on) + EMAIL_TEST_REDIRECT for the template-email channel, with explicit-on fail-safe — missing/empty/unknown env behaves as off, never breaks; test redirects non-exempt email to the test inbox; already-live bank_account_deleted / crypto_address_deleted are exempt via notification.email_exempt_slugs
profile.company.* settings auto-merged into email variables at dispatch (call-site wins) so all 74 templates render {{company.*}} centrally — EMAIL_CUTOVER_PLAN Phase 0
deposit_receipt_uploaded + deposit_expired, missed by accident between Batches 2 and 4
connection_status from admin.mt_setting.mt5.get
NotificationSettingSeeder for encrypted SendGrid configuration
dev, demo, zyz, okm, and any new tenants added later). Without this, the notification microservice has no SendGrid key to read and every email send will fail, breaking all flows that depend on email.
sendgrid_api_key in NotificationSetting model
2026_04_20_120001-120005) add the notification_template_versions table, current_version_id on templates, template_version_id + raw_body on notifications, backfill existing data, and drop the legacy body_preview column. Run for every tenant (dev, demo, zyz, okm, and any future tenants): php artisan tenant:migrate {slug}. Until these run, the notification microservice will reject new sends because it expects current_version_id to be set on every active template. Deployment order: deploy migration-service → run migrations → deploy the notification microservice → restart queue workers. Deploying the new notification code before the migrations run will cause inserts to fail because the new columns don't exist yet.
bank_account_deleted email
php artisan tenant:seed {slug} --class=NotificationTemplateSeeder for dev, demo, zyz, okm. The existing four templates are deduped via SHA-256 hash; only the new bank_account_deleted row is inserted.
ib_commission_paid email template — no commission-payout event exists in the project; dropped from Batch 8 and soft-deleted on seeded tenants via Batch9FinanceGapEmailTemplateSeeder::deactivateRemovedTemplates()
ClientController::store and createLiveAccount on admin_backend mirror the public-flow IBP pushes (auth/account microservices → /api/ibp/internal/*) with the same payloads; admin-created clients carry a null referral pair; best-effort with the same no-active-bridge skip guard, so an IBP outage never fails the admin action. Adds unit tests for IbpOutboundService (auth headers, endpoints, error logging) and feature tests for the store() push
Log::info calls across the API Gateway controllers and service classes (68 in total) that wrote full request data and response bodies to laravel.log — on the auth path these leaked user passwords (login/register/reset) and issued Bearer tokens in plaintext. Log::warning / Log::error (error handling, security events) are kept
user_id instead of the authenticated token, so any logged-in user could read or modify another user's data by changing user_id. New ForceTokenUserId middleware (force.user_id) overwrites user_id with $request->user()->id on each service's authenticated route group; backward compatible (the web UI already sends its own id), logs mismatches as apikey.user_id_override. Covers profile (verified on dev), finance, account, and helpcenter (whose downloadAttachment ownership check also compared against the tamperable user_id and now checks the token user). notification/auth were already token-derived; admin_backend is permission-gated and unaffected
git pull each of the four branches, then php artisan route:clear && php artisan config:clear (cached routes hold the old middleware list) and reload php-fpm if opcache is on.
frontend_url (server-side, resolved by ResolveTenant) instead of a caller-supplied frontend_url request field. Direct API callers such as the Flutter mobile app never sent that field, so the link base fell back to config('services.frontend_url')/localhost and produced broken links; the web UI was unaffected because ui_gateway always sent its own URL. Also removes a client-controlled URL from security emails. frontend_url is dropped from the register-submit, forgot-password, and demo-register request bodies in the client OpenAPI spec (bumped to 1.2.1.0) and User Guide
git pull on auth_microservice and reload php-fpm. Before deploying, confirm every active tenant has frontend_url populated in the gateway tenants table (dev, demo, zyz, okm, and any future tenants). It is a nullable column; a NULL value now falls back to config('services.frontend_url')/localhost, which would break web links too, not just mobile.
GET /settings documented as a plain authenticated request — removed the keys[] query parameter (the gateway forwards an empty payload, so the filter never reaches the auth microservice and the full map is always returned), and clarified that a bare GET with X-API-Key + Bearer returns the whole tenant settings {key: value} map
## Changelog section inside the spec info.description (travels with the yaml, so it shows in Postman / Swagger / the hosted docs page). Entries are tagged [contract] (re-import the spec) vs [docs] (wording only), keyed to info.version; seeded at 1.2.0.0 (baseline) and 1.2.1.0. Going forward, every client-spec change bumps info.version and adds a changelog line in the same edit
/api-docs) now renders that spec changelog as its own "Changelog" section with a sidebar link — ApiDocsController extracts the ## Changelog block from the spec's info.description (the page is built from the User Guide and previously ignored info.description, so the changelog was browser-invisible). No new deploy step beyond the existing docs-page deploy
AuthService::buildHeaders() override dropped X-API-Key, blocking login under enforce mode; now reuses base headers
status = deleted), matching the Clients list
open like Support/Funds
php artisan tenant:migrate {slug} for every active tenant. Adds the generic reports table (id, name, type, filters JSON, columns JSON, admin_id FK → admin_register, row_count, status, timestamps; indexed on (type, created_at)). One table backs all five report types — adding a new report type does NOT require another migration. Composer dependency. barryvdh/laravel-dompdf:^3.0 is now in composer.json on admin_backend. Run composer install on the admin_backend server after pulling. Required for PDF export — the controller fails with "Class Pdf not found" without it. Permission keys. Reports use the shared reports.* namespace (no per-report-type split): reports.view, reports.generate, reports.rerun, reports.delete, reports.export_pdf, reports.export_csv. Grant these to admin roles that need access; absence hides the buttons via ui.perm middleware on admin_ui. Export throttles per admin. admin_backend: throttle:5,1 (PDF), throttle:10,1 (CSV). api_gateway: throttle:30,1 (PDF), throttle:60,1 (CSV). admin_backend is the tighter gate (matches "real" load); api_gateway's looser caps absorb cancelled-redirect retries. PDF hardening. dompdf runs with isRemoteEnabled=false, all variables in PDF blades go through {{ }} (never {!! !!}), and safeLogoUrl() accepts only data:image/* or local public/ / storage/ paths — no remote logo URLs. Logo source is the per-tenant settings row keyed profile.company.logo_url. CSV hardening. Streams via php://output with UTF-8 BOM. Cell values prefixed with =, +, -, or @ are escaped with a leading single-quote to defeat formula injection in Excel / Google Sheets.
php artisan tenant:migrate {slug} for every active tenant before this release goes live. Adds read_at column + composite index on the notifications table. Without it the user feed endpoints fail with SQL errors. Template seeder required. Run php artisan tenant:seed {slug} --class=InAppTemplateSeeder for every tenant. Seeds the single generic_in_app template. Without it, the in-app channel silent-skips every send (nothing appears in the bell). Env vars required on newly-scaffolded services. account_microservice and helpcenter now talk to the notification microservice. Set NOTIFICATION_SERVICE_URL, NOTIFICATION_SERVICE_TIMEOUT, NOTIFICATION_SERVICE_ENABLED, and INTERNAL_API_KEY on both servers. INTERNAL_API_KEY must match the value on the notification microservice. Deployment order: migration-service → notification → api_gateway → ui_gateway + admin_ui → admin_backend + profile_microservice + account_microservice + finance_microservice + helpcenter. Cache clears: after deploy, run php artisan config:clear && php artisan route:clear on every service touched, plus php artisan view:clear on ui_gateway + admin_ui.
php artisan tenant:migrate {slug} for every active tenant. Adds notification_subscriptions table (admin_id, event_slug, enabled). Without it the admin preferences endpoints fail with SQL errors and the fan-out dispatcher returns "no subscribers" for every event. Env vars required on auth_microservice (new caller). auth_microservice now talks to the notification microservice for the new_user_registered admin event. Set NOTIFICATION_SERVICE_URL, NOTIFICATION_SERVICE_TIMEOUT, NOTIFICATION_SERVICE_ENABLED, and INTERNAL_API_KEY on auth_microservice. INTERNAL_API_KEY must match the value on the notification microservice. No new template seeder. Admin in-app rows render through the existing generic_in_app template (already seeded in the user-side rollout) — Path D extended to the admin surface. Default subscriptions are OFF. Existing admins start with zero subscriptions and an empty bell. They must visit /admin/notification-preferences and opt in to the events they care about. This avoids paging every admin for every event on day one. Deployment order: migration-service → notification → api_gateway → admin_ui → admin_backend + profile_microservice + finance_microservice + account_microservice + helpcenter + auth_microservice. Cache clears: after deploy, run php artisan config:clear && php artisan route:clear on every service touched, plus php artisan view:clear on admin_ui.
employees.bulk.status, employees.bulk.role, employees.bulk.delete
employees.bulk.status, employees.bulk.role, employees.bulk.delete added to PermissionsSeeder and EmployeePermissionSeeder
id_front_image / id_back_image columns on admin_register
employee_permissions.updated_at (cached 10 min + invalidated on write) instead of scanning admin_logs, and admin_ui throttles checks to once per 30s per sessionStorage key
Mail::to(...)->send(...) paths on six microservices now read SendGrid credentials from the tenant DB (notification_settings.sendgrid_api_key) instead of each container's .env MAIL_PASSWORD
NotificationSettingSeeder rows (already seeded for dev, demo, zyz, okm, and any future tenant via Phase 3 of TENANT_ONBOARDING_HSW.md) are the new source of truth. Verify notification_settings.sendgrid_api_key is populated on every active tenant before deploy. Mail is disabled (silent no-op) for any tenant without a sendgrid_api_key. The middleware switches mail.default to 'array' so Mail::to(...)->send(...) returns successfully without delivering. A warning is logged (Tenant has no sendgrid_api_key — skipping email send). Registration / password reset / OTP / etc. complete normally; only the email side-effect is suppressed. APP_KEY must be identical across the six affected microservices. The SendGrid key is encrypted at rest in notification_settings; mismatched keys cause DecryptException per-request, which the middleware logs and skips. Already enforced on the platform — flagging for completeness. Cache invalidation. Per-tenant config is cached for 1 hour under mail_config:{slug}. If you rotate a SendGrid key via SQL/seeder, run php artisan cache:clear (or cache:forget mail_config:{slug} if available) on each of the six services for instant effect. Otherwise the new key picks up within an hour. Env MAIL_PASSWORD becomes a dead value. Once the deploy is verified, MAIL_PASSWORD in each microservice's .env is no longer read by the mail path. Safe to delete after a week of monitoring; leaving it in place is harmless.
172.18.0.1 because redis-server.service started before docker.service on boot
172.18.0.0/16 + localhost
ACCEPT 0.0.0.0/0 rule on port 3306 that was leaving MariaDB exposed publicly
redis-server.service (After=docker.service, Requires=docker.service) so Redis binds to the Docker bridge gateway on every boot
config('app.name') and inline Setting::where() queries with $companyName passed from controller via Mailable
profile.company.* tenant settings
companyProfile() bulk-fetch method to EmailTemplateFlag on admin_backend
CompanyProfileSettingSeeder with 5 keys: profile.company.logo_url, profile.company.website_url, profile.company.phone, profile.company.address_physical, profile.company.address_registered
database-conventions.md SOP — use firstOrCreate for tenant-customisable settings, never modify existing seeders
MAJOR.FEATURE.IMPROVEMENT.FIX
my.oktrum.com, bo.oktrum.com, okm.api.ataria.pro)
ataria_gateway) + tenant DB (okm) with unified APP_KEY