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:

  1. Account owner — listed in the account_ownership table as an owner of that account
  2. 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:

  1. Insert a row into the accounts table with id, name, and optionally logo
  2. Insert a row into account_ownership linking the account to an owner user by accountId and ownerId

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'"