Accounts
Accounts are the top-level organizational unit in Eventuall. Every event belongs to an account, and all dashboard URLs are scoped under /dashboard/account/[accountId]/. When a user navigates to /dashboard, they're automatically redirected to their first account's event list.
How account access works
A user sees an account in the dashboard if they meet either condition:
- Account owner — listed in the
account_ownershiptable as an owner of that account - Moderator — has an access grant with the "Moderator" role on any event within that account
There is no self-service account creation. The "Add account" button in the sidebar account switcher is commented out in the codebase. New accounts are created manually at the database level.
How existing accounts were created
The accounts that exist in the platform (like the Eventuall account) were seeded directly into the database. The process is:
- Insert a row into the
accountstable withid,name, and optionallylogo - Insert a row into
account_ownershiplinking the account to an owner user byaccountIdandownerId
There is no admin UI, API endpoint, or tRPC procedure for creating accounts. This is intentional — account creation is a low-frequency operation that requires manual intervention.
Account switching
Users with access to multiple accounts see a dropdown switcher in the sidebar header (_components/account-switcher.tsx). Selecting a different account navigates to /dashboard/account/[newAccountId], which triggers a full re-render and data refetch. Users with only one account see a static display instead of a dropdown.
No-account state
If a user signs in but doesn't belong to any account (no ownership records, no Moderator grants), they're redirected to /dashboard/no-account, which shows a simple "You may not have been added to any accounts yet" message. Additionally, the /dashboard/account route checks for Moderator access before proceeding — if the user has no Moderator role at all, they're redirected to the home page.
Dashboard routing flow
/dashboard
→ getUserAccounts()
→ if no accounts → /dashboard/no-account
→ else → /dashboard/account/[firstAccountId]
/dashboard/account
→ hasModeratorAccess()
→ if no moderator role → / (home page)
→ else → getUserAccounts() → /dashboard/account/[firstAccountId]
/dashboard/account/[accountId]
→ getAccountById()
→ if account not found → /dashboard/no-account
→ else → /dashboard/account/[accountId]/event (events list)
Schema
The accounts table:
| Column | Type | Description |
|---|---|---|
id |
text (PK) | Auto-generated CUID |
name |
text | Account display name |
logo |
text | Optional logo URL |
createdAt |
timestamp | Creation date |
updatedAt |
timestamp | Last update |
The account_ownership junction table:
| Column | Type | Description |
|---|---|---|
accountId |
text (PK) | References accounts.id |
ownerId |
text (PK) | References users.id |
Composite primary key on (accountId, ownerId). An account can have multiple owners, and a user can own multiple accounts.
tRPC procedures
All account procedures live in apps/webapp/src/server/api/routers/account.ts. All are protectedProcedure (require authentication).
| Procedure | Type | Description |
|---|---|---|
account.getAccountById |
query | Returns an account if the caller is an owner or has Moderator access to an event within it |
account.getUserAccounts |
query | Returns all accounts the caller can access (owned + Moderator grants), deduplicated |
account.hasModeratorAccess |
query | Boolean — whether the caller has any Moderator-level access grant |
Key components
| Component | Path | Purpose |
|---|---|---|
| Account switcher | dashboard/_components/account-switcher.tsx |
Sidebar dropdown for switching between accounts |
| Dashboard redirect | dashboard/route.ts |
Auto-redirects to first account |
| Account gate | dashboard/account/route.ts |
Checks Moderator access before proceeding |
| No-account page | dashboard/no-account/page.tsx |
Fallback when user has no accounts |
Adding a new account
Account creation requires direct database inserts. There is no UI or API for this.
Local development:
# 1. Create the account
wrangler d1 execute eventuall-webapp-db --local --command \
"INSERT INTO accounts (id, name, createdAt, updatedAt) VALUES ('acct_myaccount', 'My Account', strftime('%s','now'), strftime('%s','now'))"
# 2. Link an owner (replace USER_ID with the actual user ID)
wrangler d1 execute eventuall-webapp-db --local --command \
"INSERT INTO account_ownership (accountId, ownerId) VALUES ('acct_myaccount', 'USER_ID')"
Remote/production:
# Same commands with --remote flag
wrangler d1 execute eventuall-webapp-db --remote --command \
"INSERT INTO accounts (id, name, createdAt, updatedAt) VALUES ('acct_myaccount', 'My Account', strftime('%s','now'), strftime('%s','now'))"
wrangler d1 execute eventuall-webapp-db --remote --command \
"INSERT INTO account_ownership (accountId, ownerId) VALUES ('acct_myaccount', 'USER_ID')"
To find a user's ID, query the users table by email:
wrangler d1 execute eventuall-webapp-db --local --command \
"SELECT id, name, email FROM users WHERE email = 'user@example.com'"