Examples
Business logic and code examples
1. Self-serve signup (sole proprietor: corporate + manager)
Anyone can Sign up without an admin. The app provisions a linked workspace: one shared id for corporates and managers (with managers.corp_id equal to that id), plus a default team, via provisionSoloManagerWorkspace in lib/data/linked-user-workspace.ts, called from signUp in actions/auth.ts. Caps for solo workspaces live in lib/workspace/solo.ts (default 2 corp-users total / 2 per team; Pro via Admin raises max_teams and caps to 40 total / per team).
- After signup,
GET /api/confirm-email?token=...confirms the manager row, then creates a session withmanagerId,corpId, andcorpName. - Sign in resolves a confirmed manager by email, else a corp-user—one email/password form (
signIninactions/auth.ts). - Account:
/dashboard/settingsandchangeUserPasswordActionupdatemanagersorcorp_users.
2. Corporate and manager registration (admin path)
Separately, corporates can be created by an admin. Managers are invited by the admin and complete registration via an email link.
Creating a corporate (admin)
The admin goes to Dashboard → Admin → Corporates → Add Corporate and submits the form (name, address, phone). The server action createCorporateAction in actions/corporates.ts runs:
requireAdmin()ensures only an admin can call it.- Form data is validated with
CorporateSchema(name, address, phone). dbCreateCorporate(fromlib/data/corporates.ts) inserts a new row into thecorporatestable with a generatednanoid(12).- Redirects to
/dashboard/admin/corporates.
Inviting a manager (admin)
From the corporates list, the admin clicks Add Manager for a corporate and enters the manager’s email. The action inviteManagerAction in actions/managers.ts:
- Checks admin and that the corporate exists; ensures no existing manager with that email for that corporate.
- Generates a temporary password and a confirmation token (24h expiry).
createManagerinlib/data/managers.tsinserts intomanagers(id, email, hashed password, corpId, emailConfirmationToken, emailConfirmationTokenExpiresAt).- Sends an invitation email with a link like
/register/manager?token=.... - A cron job can call
POST /api/clean-managers-tableto delete unconfirmed managers after 24h.
Manager completes registration
The manager opens the link and lands on /register/manager. The form is pre-filled with email and corporate (from GET /api/validate-manager-token?token=...). On submit, registerManagerAction in actions/manager-registration.ts:
- Validates token via
getManagerByConfirmationToken; rejects if invalid or email mismatch. - Hashes the new password and calls
updateManagerPassword(updates only password, keeps token for email confirmation). - Sends a confirmation email with a link to
GET /api/confirm-manager-email?token=....
When the manager clicks the confirmation link, the API validates the token, sets emailConfirmed, creates a session (managerId, corpId, corpName), and redirects to /dashboard/corporate.
3. Corporate user registration and team assignment
Corporate users (team members) are invited by a manager and are assigned to a team at invitation time. One corp-user can belong to multiple teams (many-to-many via teams_corp_users).
Manager invites a corp-user
The manager goes to Dashboard → Corporate → Corp Users → Add Corp User (or /dashboard/corporate/corp-users/new). They must have at least one team (enforced in the page). The form collects email and team. The action inviteCorpUserAction in actions/corp-users.ts:
- Requires
session.managerId. - Validates email, corpId, teamId; ensures the team belongs to the current manager and the corporate exists.
- Ensures no existing corp-user with that email (in the app, uniqueness is by email across the corporate).
createCorpUserinlib/data/corp-users.tsinserts intocorp_users(temp password, confirmation token, 24h expiry).addCorpUserToTeam(teamId, corpUserId)inlib/data/teams.tsinserts intoteams_corp_users, so the user is assigned to the chosen team at creation.- Sends invitation email with link to
/register/corp-user?token=.... Confirmation link isGET /api/confirm-corp-user-email?token=...→ sets email confirmed, creates session, redirects to/dashboard/issues.
4. Manager: updating teams and users
From Dashboard → Corporate, the manager has a Teams tab and a Corp Users tab. Teams are created, edited, and deleted; users can be added to teams.
Teams: create, update, delete
All team actions live in actions/teams.ts and require either a manager session or admin.
- Create:
createTeamAction(corpId, prev, formData)— requiressession.managerId, validates name (1–100 chars), thendbCreateTeaminlib/data/teams.ts(name, managerId, corpId). Redirects to/dashboard/corporate. - Update:
updateTeamAction(teamId, prev, formData)— loads team; only the team’s manager (or admin) can update.dbUpdateTeam(teamId, { name })updates the name. - Delete:
deleteTeamAction(teamId)— same auth;dbDeleteTeam(teamId)removes the team (and any junction rows inteams_corp_usersby schema/constraints).
Adding a user to a team
On the team edit page (/dashboard/corporate/teams/[id]/edit), the manager sees current members and a dropdown of corp-users not in the team. Selecting one and submitting calls addCorpUserToTeamAction(teamId, corpUserId):
- Verifies the team belongs to the current manager.
- Verifies the corp-user belongs to the same corporate (
getCorpUsersByCorpId(team.corpId)). dbAddCorpUserToTeam(teamId, corpUserId)inserts intoteams_corp_users. The data layer also exposesremoveCorpUserFromTeam(teamId, corpUserId)inlib/data/teams.tsfor removing a user from a team.
5. Issues: create, assign, filter, update
Issues are usually created in a corporate context: a team (optional for some rows) with a corp-user creator (corpUserId) or a manager creator (createdByManagerId). Personal issues set userId to the owning manager’s id; corporate is derived from that manager’s corp_id for list/detail and getIssueCorporateId in actions). Assignees use assignedCorpUserId / assignedManagerId.
Creating an issue
createIssue(data) in actions/issues.ts validates with IssueSchema: title (3–100), description, status (backlog | todo | in_progress | done), priority (low | medium | high). Then:
- Corp-user:
corpUserId+ validteamIdfor their teams; session must be that corp-user. - Manager:
createdByManagerId+teamIdthe manager owns (getTeamsByManagerId); session must be that manager.
Updating an issue
updateIssue(id, data) first authorizes the caller:
- Corp-user:
authorizeCorpUserToEditIssue(corpUserId, issueId)inlib/data/issues.ts— creator, assignee, or team membership. - Manager:
updateIssueloadsgetIssueCorporateId(issueId)and requires it to equalsession.corpId(same resolution as the issue detail page, including personaluserIdissues). - Then only title, description, status, and priority are updated (no owner/assignee change here).
Assigning an issue to a user or manager
assignIssueAction(issueId, assigneeType, assigneeId) in actions/issues.ts:
- Loads the issue; it must have a
teamId. Resolves the team’scorpId. - Caller must be a manager who owns that team, or a corp-user with edit permission on the issue (
authorizeCorpUserToEditIssue). - If
assigneeType === nullor no assigneeId:assignIssue(issueId, null, null)clears assignment. - If
assigneeType === 'corp_user': assignee must be ingetAssignableCorpUsersByCorpId(corpId); thenassignIssue(issueId, assigneeId, null)setsassignedCorpUserId. - If
assigneeType === 'manager': assignee must be ingetManagersByCorpId(corpId); thenassignIssue(issueId, null, assigneeId)setsassignedManagerId.
Filtering issues
The issues list is built in app/dashboard/issues/page.tsx and filtered in IssuesContent (app/dashboard/issues/IssuesContent.tsx).
- Corp-user: Tab “Created by me” →
getIssuesCreatedByCorpUser(corpUserId); “Assigned to me” →getIssuesAssignedToCorpUser(corpUserId). Optional?teamId=filters to that team (client-side filter on the fetched list). - Manager: Teams from
getTeamsByCorpId(session.corpId). With All teams, the server merges per-team issues plusgetIssuesWithNoTeamByCorpId(includes no-team rows tied to corp-users/managers in the corp, includingissues.user_idpersonal issues for managers in that corp). With a specific corp-user,getIssuesAssignedToCorpUserplus team / no-team filters.createIssuestill requires the manager to own the chosen team viagetTeamsByManagerIdwhere enforced. - Search: Query param
q— client-side filter onissue.title(case-insensitive). - Status: Query param
status(backlog | todo | in_progress | done) — client-side filter.
6. Admin: all issues (paginated)
Dashboard → Admin → All Issues (/dashboard/admin/issues) loads a page of every issue with getAllIssuesPaginated from lib/data/admin.ts. Query params: page (default 1) and perPage (default 50). Pagination controls live in AdminIssuesList.tsx.
For API endpoints used by these flows (e.g. confirm-email, validate-manager-token, teams), see the API Reference.