SavvySolve Docs

Solver Applications

Public application system for prospective solvers to join the SavvySolve platform

Solver Applications

The solver applications feature provides a structured process for recruiting new solvers. Prospective solvers submit applications through a public form, which admins review and approve before the applicant can create their account.

How It Works

The application workflow follows these stages:

  1. Application Submission - Prospective solver fills out the multi-step application form at /apply
  2. Admin Review - Platform admins review applications in the admin dashboard
  3. Approval/Rejection - Admin approves or rejects with optional notes
  4. Account Creation - Approved applicants sign up via Clerk, automatically becoming solvers
  5. Onboarding - New solvers complete onboarding checklist before going live

This flow ensures quality control while keeping the barrier to entry reasonable for qualified candidates.

Application Form

The public application form at /apply collects information across four steps:

Step 1: Contact Information

Basic contact details for communication and identity verification.

lib/db/schema/solver-applications.ts
export const solverApplications = pgTable("solver_applications", {
  id: uuid("id").primaryKey().defaultRandom(),
  name: varchar("name", { length: 255 }).notNull(),
  email: varchar("email", { length: 255 }).notNull().unique(),
  phone: varchar("phone", { length: 50 }).notNull(),
  // ...
});

Step 2: Experience & Motivation

Free-text fields capturing the applicant's background in tech support and why they want to join SavvySolve.

Step 3: Availability

A weekly schedule grid where applicants indicate their typical available hours. This helps match solvers with demand patterns.

lib/db/schema/solver-applications.ts
export interface ApplicationAvailability {
  monday: string[];
  tuesday: string[];
  wednesday: string[];
  thursday: string[];
  friday: string[];
  saturday: string[];
  sunday: string[];
}

Each day contains an array of time slots (e.g., ["morning", "afternoon", "evening"]).

Step 4: Technical Skills Assessment

Self-reported comfort levels across common tech support categories:

lib/db/schema/solver-applications.ts
export interface TechSkillsAssessment {
  mobileDevices: number; // 1-5
  computers: number;
  networking: number;
  software: number;
  printers: number;
  email: number;
  security: number;
}

Database Schema

Applications are stored with full audit trail:

lib/db/schema/solver-applications.ts
export const applicationStatusEnum = pgEnum("application_status", [
  "pending",
  "approved",
  "rejected",
  "withdrawn",
]);

export const solverApplications = pgTable("solver_applications", {
  id: uuid("id").primaryKey().defaultRandom(),
  
  // Contact info
  name: varchar("name", { length: 255 }).notNull(),
  email: varchar("email", { length: 255 }).notNull().unique(),
  phone: varchar("phone", { length: 50 }).notNull(),
  
  // Experience
  experience: text("experience").notNull(),
  motivation: text("motivation").notNull(),
  
  // Structured data
  availability: jsonb("availability").$type<ApplicationAvailability>().notNull(),
  techSkills: jsonb("tech_skills").$type<TechSkillsAssessment>().notNull(),
  
  // Review workflow
  status: applicationStatusEnum("status").notNull().default("pending"),
  reviewedBy: uuid("reviewed_by"),
  reviewedAt: timestamp("reviewed_at", { withTimezone: true }),
  reviewNotes: text("review_notes"),
  
  // Link to user after approval + signup
  userId: uuid("user_id"),
  
  // Timestamps
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});

API Reference

The applications router provides these tRPC procedures:

applications.submit

Public procedure for submitting new applications.

// Input schema
z.object({
  name: z.string().min(2).max(255),
  email: z.string().email(),
  phone: z.string().min(10),
  experience: z.string().min(50),
  motivation: z.string().min(50),
  availability: availabilitySchema,
  techSkills: techSkillsSchema,
})

// Returns
{ success: true, applicationId: string }

applications.list (Admin)

Lists applications with optional status filtering and pagination.

// Input
z.object({
  status: z.enum(["pending", "approved", "rejected", "withdrawn"]).optional(),
  limit: z.number().min(1).max(100).optional().default(20),
  offset: z.number().min(0).optional().default(0),
})

// Returns
{
  applications: SolverApplication[],
  total: number,
  hasMore: boolean,
}

applications.get (Admin)

Retrieves a single application by ID with full details.

applications.approve (Admin)

Approves an application, optionally with notes.

// Input
z.object({
  applicationId: z.string().uuid(),
  notes: z.string().optional(),
})

applications.reject (Admin)

Rejects an application with required notes explaining the decision.

// Input
z.object({
  applicationId: z.string().uuid(),
  notes: z.string().min(10),
})

applications.getPendingCount (Admin)

Returns the count of pending applications for sidebar badges.

Admin Review Interface

Admins access the application review interface at /admin/applications. The interface provides:

  • Filtering - View all, pending, approved, or rejected applications
  • Pagination - Navigate through applications
  • Detail View - Modal showing full application with availability grid and skills chart
  • Actions - Approve or reject with notes

The admin sidebar shows a badge with the pending application count, alerting admins to new applications that need review.

Clerk Webhook Integration

When an approved applicant creates their account via Clerk, the webhook handler automatically:

  1. Checks if the email matches an approved application
  2. Creates the user record with role: "solver"
  3. Creates the solver record with status: "onboarding"
  4. Links the application to the new user
app/api/webhooks/clerk/route.ts
// Check if this email has an approved solver application
const approvedApplication = await db.query.solverApplications.findFirst({
  where: and(
    eq(solverApplications.email, email),
    eq(solverApplications.status, "approved")
  ),
});

if (approvedApplication) {
  role = "solver";
  // Link application to user after insert
}

This eliminates manual intervention—approved applicants automatically become solvers upon signup.

Security Considerations

  • Application submission is public (no auth required)
  • Email uniqueness prevents duplicate applications
  • Admin-only procedures use adminProcedure middleware
  • Review actions are audited with reviewer ID and timestamp

On this page