Skip to main content
Buildstory uses Next.js server actions for all data mutations. All actions follow a consistent pattern with type-safe inputs, authentication, error handling, and Sentry integration.

Action Pattern

All server actions return an ActionResult<T> type:
type ActionResult<T = undefined> =
  | { success: true; data?: T }
  | { success: false; error: string };

Common Utilities

// Get authenticated profile ID
async function getProfileId(): Promise<string> {
  const { userId } = await auth();
  if (!userId) throw new Error("Not authenticated");
  const profile = await ensureProfile(userId);
  if (!profile) throw new Error("Profile creation failed");
  return profile.id;
}

// Error handling pattern
try {
  const profileId = await getProfileId();
  // ... business logic
  revalidatePath("/path");
  return { success: true };
} catch (error) {
  if (isUniqueViolation(error, "constraint_name")) {
    return { success: false, error: "User-friendly message" };
  }
  Sentry.captureException(error, {
    tags: { component: "server-action", action: "actionName" },
    extra: { /* context */ },
  });
  return { success: false, error: "Generic error message" };
}

Authentication & Onboarding

checkUsernameAvailability

Check if a username is available (unauthenticated). Location: app/(onboarding)/hackathon/actions.ts:44
username
string
required
Username to check
const result = await checkUsernameAvailability("alice");
// { success: true, data: { available: true } }
available
boolean
Whether the username is available

completeRegistration

Complete hackathon registration and update profile. Location: app/(onboarding)/hackathon/actions.ts:73
displayName
string
required
User’s display name
username
string
required
Unique username
country
string | null
ISO 3166-1 alpha-2 country code
region
string | null
ISO 3166-2 subdivision code
experienceLevel
'getting_started' | 'built_a_few' | 'ships_constantly'
required
Experience level
commitmentLevel
'all_in' | 'daily' | 'nights_weekends' | 'not_sure' | null
Availability commitment
teamPreference
'solo' | 'has_team' | 'has_team_open' | 'looking_for_team'
required
Team status
eventId
string
required
Event UUID

createOnboardingProject

Create a project during hackathon onboarding. Location: app/(onboarding)/hackathon/actions.ts:144
name
string
required
Project name
slug
string
required
URL-safe slug
description
string
required
Project description
startingPoint
'new' | 'existing'
required
Whether project is new or continuing
goalText
string
required
Project goal
repoUrl
string
required
GitHub repository URL
eventId
string
required
Event UUID

Profile Management

updateProfile

Update user profile settings. Location: app/(app)/settings/actions.ts:63
displayName
string
required
Display name
username
string
required
Username (must be unique)
bio
string | null
Profile bio
websiteUrl
string | null
Website URL
twitterHandle
string | null
Twitter/X handle
githubHandle
string | null
GitHub username
twitchUrl
string | null
Twitch URL
streamUrl
string | null
Other stream URL
country
string | null
Country code
region
string | null
Region/subdivision code
experienceLevel
'getting_started' | 'built_a_few' | 'ships_constantly' | null
Experience level
allowInvites
boolean
Allow team invites (privacy setting)

syncAvatarUrl

Sync avatar URL from Clerk. Location: app/(app)/settings/actions.ts:18
avatarUrl
string | null
required
Clerk avatar URL or null to remove

Project Management

createProject

Create a new project. Location: app/(app)/projects/actions.ts:27
name
string
required
Project name
slug
string
required
URL-safe slug (must be unique)
description
string
required
Project description
startingPoint
'new' | 'existing'
required
Starting point
goalText
string
required
Project goal
repoUrl
string
required
GitHub repository URL
liveUrl
string
required
Live demo URL
eventId
string
Optional event ID to link project
slug
string | null
Created project slug

updateProject

Update an existing project (owner only). Location: app/(app)/projects/actions.ts:94
projectId
string
required
Project UUID
Other parameters identical to createProject

deleteProject

Delete a project and all related data (owner only). Location: app/(app)/projects/actions.ts:165
projectId
string
required
Project UUID

Team Management

sendDirectInvite

Send a direct team invite to a specific user. Location: app/(app)/projects/[slug]/team-actions.ts:40
projectId
string
required
Project UUID
recipientUsername
string
required
Recipient’s username
Generate a shareable invite link. Location: app/(app)/projects/[slug]/team-actions.ts:124
projectId
string
required
Project UUID
token
string
Invite token (UUID v4)

respondToInvite

Accept or decline a direct invite. Location: app/(app)/projects/[slug]/team-actions.ts:166
inviteId
string
required
Invite UUID
accept
boolean
required
Whether to accept the invite
Accept a link-based invite. Location: app/(app)/projects/[slug]/team-actions.ts:219
token
string
required
Invite token
projectSlug
string | null
Project slug after joining

revokeInvite

Revoke a pending invite (sender only). Location: app/(app)/projects/[slug]/team-actions.ts:289
inviteId
string
required
Invite UUID

removeTeamMember

Remove a team member (project owner only). Location: app/(app)/projects/[slug]/team-actions.ts:322
projectId
string
required
Project UUID
memberId
string
required
Profile UUID of member to remove

leaveProject

Leave a project as a team member. Location: app/(app)/projects/[slug]/team-actions.ts:357
projectId
string
required
Project UUID
Project owners cannot leave their own project.

searchUsersForInvite

Search for users to invite to a project. Location: app/(app)/projects/[slug]/team-actions.ts:391
projectId
string
required
Project UUID
query
string
required
Search query (min 2 characters)
results
array
Array of user objects with id, displayName, username

Admin Actions

hideUser / unhideUser

Hide or unhide a user from public listings (moderator+). Location: app/admin/users/actions.ts:36 | actions.ts:76
profileId
string
required
Profile UUID to hide/unhide

banUser / unbanUser

Ban or unban a user (moderator to ban, admin to unban). Location: app/admin/users/actions.ts:113 | actions.ts:184
profileId
string
required
Profile UUID to ban/unban
reason
string
Ban reason (optional)

deleteUser

Permanently delete a user and all data (admin only). Location: app/admin/users/actions.ts:239
profileId
string
required
Profile UUID to delete

setUserRole

Change a user’s role (admin only). Location: app/admin/roles/actions.ts:14
profileId
string
required
Profile UUID
role
'user' | 'moderator' | 'admin'
required
New role

searchProfilesByName

Search profiles by name or username (admin only). Location: app/admin/roles/actions.ts:76
query
string
required
Search query (min 2 characters)
results
array
Array of profiles with id, displayName, username, avatarUrl, role, clerkId

Search Actions

searchUsers

Search users by username or display name. Location: app/(onboarding)/hackathon/actions.ts:208
query
string
required
Search query (min 2 characters)
results
array
Array of users with id, displayName, username

searchProjects

Search projects by name. Location: app/(onboarding)/hackathon/actions.ts:245
query
string
required
Search query (min 2 characters)
profileId
string
Optional profile ID to filter by owner
results
array
Array of projects with id, name, description

checkProjectSlugAvailability

Check if a project slug is available. Location: app/(onboarding)/hackathon/actions.ts:283
slug
string
required
Slug to check (min 2 chars, alphanumeric + hyphens)
available
boolean
Whether the slug is available

Error Handling

Unique Constraint Violations

All actions use the isUniqueViolation() helper to detect duplicate key errors:
import { isUniqueViolation } from "@/lib/db/errors";

try {
  await db.insert(profiles).values({ username: "alice" });
} catch (error) {
  if (isUniqueViolation(error, "profiles_username_unique")) {
    return { success: false, error: "Username is already taken" };
  }
  throw error;
}

Sentry Integration

All errors are captured with context:
Sentry.captureException(error, {
  tags: {
    component: "server-action",
    action: "actionName",
  },
  extra: {
    projectId: data.projectId,
    // ... other context
  },
});

Best Practices

  1. Always revalidate paths after mutations
  2. Use transactions for multi-table operations
  3. Validate ownership before mutations
  4. Rate limit sensitive operations
  5. Atomically claim contested resources (invites, etc.)
  6. Non-blocking external calls (Discord, Clerk) with Sentry fallback
  7. Type-safe inputs with Zod validation via parseInput()

Build docs developers (and LLMs) love