SavvySolve Docs

Customer Accounts

Optional customer accounts for session history, ratings, and faster future sessions

Customer Accounts

SavvySolve is designed for friction-free entry—customers can get help without creating an account. However, customers who create accounts gain access to their session history, can rate their solvers, and enjoy a smoother experience for future sessions.

Design Philosophy

The PRD emphasizes that the intake flow must be as simple as possible. Many customers seeking tech support are already frustrated; adding account creation would increase friction and abandonment.

Our solution: optional post-session sign-up. After submitting a ticket, customers see a prompt to create a free account. If they do, all their sessions (past and future) are automatically linked by phone number.

How It Works

Ticket Submission Flow

When a customer submits a ticket through the intake form, they provide:

  • Name
  • Phone number
  • Email (optional)
  • Problem description
  • Device type
  • Urgency level

No account is required. The ticket is created and a solver can claim it immediately.

components/IntakeForm.tsx
// After successful submission, show the confirmation with sign-up CTA
if (isSubmitted && ticketId) {
  return <TicketConfirmation ticketId={ticketId} phone={submittedPhone} />;
}

The Sign-Up Prompt

After submission, the TicketConfirmation component displays:

  1. Success message with ticket ID
  2. Optional sign-up call-to-action
  3. Benefits of creating an account
components/intake/TicketConfirmation.tsx
// Build sign-up URL with return path and phone for pre-fill
const signUpUrl = `/sign-up?redirect_url=/account&phone=${encodeURIComponent(phone)}`;

The phone number is passed to the sign-up page so Clerk can pre-fill it, making registration faster.

Auto-Linking by Phone

When a customer creates an account, the Clerk webhook automatically links all their previous tickets:

app/api/webhooks/clerk/route.ts
// Check if there are unlinked tickets with this phone number
const unlinkedTickets = await db.query.tickets.findMany({
  where: isNull(tickets.customerId),
});

const matchingTickets = unlinkedTickets.filter(
  (t) => t.customerInfo.phone === phone
);

if (matchingTickets.length > 0) {
  // User has submitted tickets before - they're a customer
  role = "customer";
}

// Link all matching tickets to the new customer
for (const ticket of matchingTickets) {
  await db.update(tickets)
    .set({ customerId: newUser.id })
    .where(eq(tickets.id, ticket.id));
}

This means:

  • If a customer signs up with the same phone they used for tickets, those tickets are automatically linked
  • Their session history is immediately available
  • No manual linking required

Database Schema

The customer account feature adds a customerId column to the tickets table:

lib/db/migrations/0005_customer_account_link.sql
ALTER TABLE tickets ADD COLUMN customer_id UUID REFERENCES users(id) ON DELETE SET NULL;

This creates a one-to-many relationship: one customer can have many tickets.

lib/db/schema/tickets.ts
export const tickets = pgTable("tickets", {
  // ... other fields
  customerId: uuid("customer_id").references(() => users.id, {
    onDelete: "set null",
  }),
});

Customer Dashboard

Authenticated customers can access /account to see their profile and session history.

Account Page (/account)

The main account page shows:

  • Profile information (name, email, phone)
  • Session statistics (total sessions, completed sessions)
  • Recent sessions with status and ratings

Session History (/account/sessions)

A dedicated page for viewing all past sessions with:

  • Expandable session cards
  • Ticket description and status
  • Solver name
  • Duration and tier
  • Rating given (if any)
  • Infinite scroll pagination
server/routers/customers.ts
// Get customer's session history with pagination
getSessions: protectedProcedure
  .input(z.object({
    limit: z.number().min(1).max(50).default(10),
    cursor: z.string().uuid().optional(),
  }))
  .query(async ({ ctx, input }) => {
    const user = await db.query.users.findFirst({
      where: eq(users.clerkId, ctx.auth.userId),
    });
    
    // Get tickets with sessions, ratings, and solver info
    const customerTickets = await db.query.tickets.findMany({
      where: eq(tickets.customerId, user.id),
      with: {
        sessions: {
          with: { solver: { with: { user: true } }, rating: true },
        },
      },
    });
    // ... flatten and return paginated sessions
  })

tRPC Procedures

The customersRouter provides these procedures:

ProcedurePurpose
getProfileGet customer profile with session stats
updateProfileUpdate name or phone number
getSessionsGet paginated session history
getTicketsGet customer's tickets with sessions
linkTicketManually link a ticket by ID + phone verification
linkAllTicketsByPhoneAuto-link all tickets matching phone

Role-Based Access

The customer routes use a role guard to ensure only customers (or higher roles) can access them:

app/(customer)/layout.tsx
export default async function CustomerLayout({ children }) {
  // Require customer role to access these routes
  await roleGuard("customer", "/");
  
  return (
    <div>
      <header>...</header>
      <main>{children}</main>
    </div>
  );
}

The role hierarchy is: customer < solver < admin < owner. A solver can access customer routes, but a customer cannot access solver routes.

Testing

The customer account feature is covered by these test files:

FileCoverage
components/intake/TicketConfirmation.test.tsxSign-up CTA UI, link generation
server/routers/customers.test.tsAll 6 tRPC procedures
app/api/webhooks/clerk/route.test.tsAuto-linking on sign-up
server/routers/customers.test.ts
describe("customersRouter", () => {
  it("returns customer profile with stats", async () => {
    mockUsersFindFirst.mockResolvedValueOnce(mockUser);
    mockTicketsFindMany.mockResolvedValueOnce([
      { sessions: [{ status: "completed" }, { status: "active" }] },
    ]);

    const result = await caller.getProfile();
    
    expect(result.stats.totalSessions).toBe(2);
    expect(result.stats.completedSessions).toBe(1);
  });
});

Future Enhancements

Potential improvements for customer accounts:

  1. Email notifications - Session updates, payment receipts
  2. Saved payment methods - Faster checkout for repeat customers
  3. Favorite solvers - Request specific solvers for future sessions
  4. Session transcripts - Download chat history from past sessions

On this page