Add multi-tenant account support with a dashboard shell to a Phoenix 1.8 app that already has phx.gen.auth and DaisyUI installed.
Prerequisites
Verify before starting:
mix phx.newcompletedmix phx.gen.auth Accounts User userscompleted- DaisyUI installed (vendor JS plugin approach)
App Name Detection
Detect the app module name from mix.exs (e.g., MyApp) and the OTP app name (e.g., my_app). Replace all occurrences of MyApp/my_app/MyAppWeb/my_app_web in reference templates with the actual names.
Implementation Order
Execute in this exact order. Read the referenced file for each step's code templates.
Phase 1: Data Layer
- Create migration for
accountstable (name, slug, status, timestamps) - Create migration for
account_usersjoin table (role, user_id, account_id, unique index) - Create
Accountschema with slug auto-generation from name - Create
AccountUserschema with role enum
Phase 2: Context Layer
- Add query functions to
Accountscontext:list_user_accounts/1,get_account/1,get_account_user/2 - Create
AccountContextmodule (facade for account-scoped access)
Phase 3: Auth Integration
Read references/auth.md
- Modify
renew_session/2inUserAuthto preservelast_account_id - Add
:load_account_contexton_mount hook toUserAuth - Create
LoadAccountplug - Create
DashboardRedirectController
Phase 4: UI Shell
- Replace
assets/css/app.csswith themed version (DaisyUI light/dark themes, sidebar styles, responsive table) - Update
root.html.heexwith theme initialization script
- Update
layouts.exwith sidebar components, theme toggle, mobile sidebar JS - Create
dashboard.html.heexlayout template
- Create
AccountSwitcherLiveComponent - Add
breadcrumbs/1component tocore_components.ex
Phase 5: Routes & Pages
Read references/router-seeds.md
- Add router configuration (
:load_accountpipeline, account-scoped live_session, dashboard redirect) - Create
DashboardLive - Create seed data (test user + 2 accounts + memberships)
- Run migrations and verify with
mix test
Key Patterns
Every dashboard LiveView must assign in mount/3:
:active_nav— atom matching sidebar item:breadcrumbs— list of%{label: "...", to: path}maps (last item has no:to):page_title— browser tab title
Breadcrumb structure:
[
%{label: "Parent", to: ~p"/dashboard/accounts/#{account.id}/parent"},
%{label: "Current Page"} # Last item — no :to key
]
Page markup pattern:
<div class="space-y-6">
<div class="flex items-end justify-between">
<div>
<h1 class="text-xl font-bold tracking-tight text-base-content">Title</h1>
<p class="mt-0.5 text-sm text-base-content/50">Subtitle.</p>
</div>
</div>
<div class="rounded-xl border border-base-300/60 bg-base-100">
<div class="flex items-center justify-between border-b border-base-300/40 px-5 py-3.5">
<h2 class="text-sm font-semibold text-base-content">Section</h2>
</div>
<div class="p-5"><!-- content --></div>
</div>
</div>
Typography scale:
- Page title:
text-xl font-bold tracking-tight text-base-content - Subtitle:
text-sm text-base-content/50 - Card title:
text-sm font-semibold text-base-content - Label:
text-xs font-medium text-base-content/50 uppercase tracking-wide - Meta:
text-xs text-base-content/40