drop-customer-support-spec
Drop Customer Support System — Implementation Spec
Project: Drop Fintech App MC Task: #1187 Created: 2026-02-17 Architecture: Software Architect Agent
1. Overview
This spec defines a built-in customer support ticket system for Drop. No external dependencies (no Zendesk, no Intercom). MVP scope.
Scope:
- Support ticket creation (user-initiated)
- Ticket conversation thread (user + admin messages)
- Admin ticket management dashboard
- Integration with audit logging
- Email notifications when admin responds
- Link tickets to transactions when relevant
Non-goals (future):
- Live chat
- Chatbot / AI responses
- Multi-language support in UI (Norwegian only for MVP)
- File attachments (future enhancement)
- Public knowledge base / FAQ system
2. Database Schema
2.1 support_tickets Table
CREATE TABLE IF NOT EXISTS support_tickets (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
subject TEXT NOT NULL,
description TEXT NOT NULL,
category TEXT NOT NULL CHECK(category IN ('transaction_issue','account_access','verification','general','dispute')),
status TEXT DEFAULT 'open' CHECK(status IN ('open','in_progress','waiting_user','resolved','closed')),
priority TEXT DEFAULT 'normal' CHECK(priority IN ('low','normal','high','urgent')),
transaction_id TEXT REFERENCES transactions(id), -- optional link to transaction
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
resolved_at TEXT,
closed_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_support_tickets_user ON support_tickets(user_id);
CREATE INDEX IF NOT EXISTS idx_support_tickets_status ON support_tickets(status);
CREATE INDEX IF NOT EXISTS idx_support_tickets_category ON support_tickets(category);
CREATE INDEX IF NOT EXISTS idx_support_tickets_created ON support_tickets(created_at);
CREATE INDEX IF NOT EXISTS idx_support_tickets_transaction ON support_tickets(transaction_id);
PostgreSQL version:
- Replace
datetime('now')withCURRENT_TIMESTAMP - No other changes needed (TEXT and CHECK constraints work in both)
2.2 ticket_messages Table
CREATE TABLE IF NOT EXISTS ticket_messages (
id TEXT PRIMARY KEY,
ticket_id TEXT NOT NULL REFERENCES support_tickets(id) ON DELETE CASCADE,
sender_type TEXT NOT NULL CHECK(sender_type IN ('user','admin')),
sender_id TEXT, -- user_id or admin user_id (optional, for audit)
message TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_ticket_messages_ticket ON ticket_messages(ticket_id);
CREATE INDEX IF NOT EXISTS idx_ticket_messages_created ON ticket_messages(created_at);
PostgreSQL version:
- Replace
datetime('now')withCURRENT_TIMESTAMP
3. Status Flow
open → in_progress → waiting_user → resolved → closed
↓ ↓ ↓ ↓
└─────────┴─────────────┴─────────────┘
(can reopen if needed)
Status definitions:
- open — newly created, not assigned yet
- in_progress — admin is working on it
- waiting_user — admin replied, waiting for user response
- resolved — issue resolved, pending user confirmation
- closed — ticket closed (auto-close after 7 days resolved, or manual)
Priority:
- urgent — transaction blocked, account locked
- high — verification failed, payment issue
- normal — general inquiry, feature request
- low — informational
4. API Endpoints
4.1 User Endpoints
POST /api/support/tickets
Create new support ticket.
Request:
{
"subject": "Transaction failed but money was deducted",
"description": "I tried to send 500 NOK to Serbia but...",
"category": "transaction_issue",
"transaction_id": "tx_rem_123" // optional
}
Response (201):
{
"data": {
"id": "tkt_abc123",
"subject": "...",
"description": "...",
"category": "transaction_issue",
"status": "open",
"priority": "normal",
"created_at": "2026-02-17T10:30:00Z"
}
}
Validation:
- subject: required, 5-200 chars, sanitized
- description: required, 20-2000 chars, sanitized
- category: must be one of enum values
- transaction_id: optional, must exist if provided
Auto-priority logic:
- category=dispute → priority=high
- category=account_access → priority=high
- category=transaction_issue → priority=normal
- category=verification → priority=normal
- category=general → priority=low
Side effects:
- Audit log:
support.ticket_created - Initial message in ticket_messages (sender_type=user, message=description)
GET /api/support/tickets
List user's tickets.
Query params:
page(default: 1)limit(default: 10, max: 50)status(optional filter: open, in_progress, waiting_user, resolved, closed)
Response:
{
"data": [
{
"id": "tkt_abc123",
"subject": "Transaction failed...",
"category": "transaction_issue",
"status": "in_progress",
"priority": "normal",
"created_at": "2026-02-17T10:30:00Z",
"updated_at": "2026-02-17T11:00:00Z",
"unread_messages": 2 // count of admin messages since last user visit
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 5,
"totalPages": 1
}
}
GET /api/support/tickets/[id]
Get ticket detail + conversation thread.
Response:
{
"data": {
"ticket": {
"id": "tkt_abc123",
"subject": "Transaction failed...",
"description": "I tried to send...",
"category": "transaction_issue",
"status": "in_progress",
"priority": "normal",
"transaction_id": "tx_rem_123",
"created_at": "2026-02-17T10:30:00Z",
"updated_at": "2026-02-17T11:00:00Z"
},
"messages": [
{
"id": "msg_1",
"sender_type": "user",
"message": "I tried to send...",
"created_at": "2026-02-17T10:30:00Z"
},
{
"id": "msg_2",
"sender_type": "admin",
"message": "Thank you for reporting. We are investigating...",
"created_at": "2026-02-17T11:00:00Z"
}
],
"transaction": { /* if transaction_id is set */ }
}
}
- User can only view their own tickets
- Return 404 if ticket belongs to another user
POST /api/support/tickets/[id]/messages
Add user message to conversation.
Request:
{
"message": "I tried again and it worked this time"
}
Response (201):
{
"data": {
"id": "msg_3",
"ticket_id": "tkt_abc123",
"sender_type": "user",
"message": "I tried again...",
"created_at": "2026-02-17T12:00:00Z"
}
}
Side effects:
- Update ticket.updated_at
- If status was 'waiting_user' or 'resolved', change to 'open'
- Audit log:
support.message_added
4.2 Admin Endpoints
All admin endpoints require requireAdmin() middleware (TBD: define admin role or separate admin auth).
GET /api/admin/support/tickets
List ALL tickets with filters.
Query params:
page,limitstatus(filter)priority(filter)category(filter)sort(created_at_desc, created_at_asc, updated_at_desc, priority_desc)
Response:
{
"data": [
{
"id": "tkt_abc123",
"user_id": "usr_demo1",
"user_email": "[email protected]",
"subject": "Transaction failed...",
"category": "transaction_issue",
"status": "open",
"priority": "normal",
"created_at": "2026-02-17T10:30:00Z",
"updated_at": "2026-02-17T11:00:00Z"
}
],
"pagination": { ... }
}
PATCH /api/admin/support/tickets/[id]
Update ticket status, priority, or admin notes.
Request:
{
"status": "in_progress",
"priority": "high"
}
Response:
{
"data": { /* updated ticket */ }
}
Side effects:
- Update ticket.updated_at
- If status changed to 'resolved', set resolved_at
- If status changed to 'closed', set closed_at
- Audit log:
support.ticket_updated
POST /api/admin/support/tickets/[id]/messages
Admin reply to ticket.
Request:
{
"message": "Thank you for reporting. We have issued a refund.",
"change_status": "resolved" // optional
}
Response (201):
{
"data": {
"id": "msg_4",
"ticket_id": "tkt_abc123",
"sender_type": "admin",
"message": "Thank you for reporting...",
"created_at": "2026-02-17T13:00:00Z"
}
}
Side effects:
- Update ticket.updated_at
- If change_status provided, update ticket.status
- Change status to 'waiting_user' if not explicitly set
- Audit log:
support.admin_reply - Email notification to user (see section 5)
5. Email Notifications
When admin replies, send email to user:
Subject: "Svar på din support-henvendelse [#{ticket_id}]"
Body (plain text):
Hei,
Vi har svart på din support-henvendelse:
Emne: {ticket.subject}
Svar: {admin_message}
Logg inn på Drop for å se hele samtalen:
{NEXT_PUBLIC_APP_URL}/support/tickets/{ticket_id}
Hvis du har flere spørsmål, kan du svare direkte i billetten.
Vennlig hilsen,
Drop Support
Implementation:
- Use existing MCP email integration pattern (see ~/system/tools/manifest.md)
- No need to implement email in MVP — create draft and leave as TODO for now
- Future: use
email-to-ticket.jspattern from ALAI tools
6. UI Pages
6.1 /support — Help Center (User)
Layout:
- Header: "Trenger du hjelp?"
- FAQ section (static, hardcoded for MVP):
- "Hvordan sender jeg penger?"
- "Hvorfor ble betalingen min avvist?"
- "Hvordan verifiserer jeg kontoen min?"
- "Hva er gebyrene?"
- Each FAQ expands to show answer (accordion)
- CTA button: "Opprett support-billett"
Design:
- Match existing Drop UI (see mockups/figma-make-export/)
- Green accent color (#00E5A0)
- Dark background (#09090b)
6.2 /support/tickets — Ticket List (User)
Layout:
- Header: "Mine support-billetter"
- Filter tabs: All | Open | Resolved | Closed
- Ticket list:
- Subject (clickable)
- Category badge
- Status badge (color-coded)
- Created date
- Unread indicator if admin replied
Empty state:
- "Ingen billetter ennå"
- "Opprett billett" button
6.3 /support/tickets/[id] — Conversation (User)
Layout:
- Ticket header:
- Subject
- Status badge
- Category
- Created date
- Transaction link (if transaction_id set): "Relatert transaksjon: [link]"
- Message thread (chat-style):
- User messages (left-aligned, gray background)
- Admin messages (right-aligned, green accent)
- Timestamps
- Reply form (if status != closed):
- Text area (5 rows)
- "Send svar" button
- Disabled if status=closed with message "Denne billetten er lukket"
6.4 /support/new — Create Ticket (User)
Form fields:
- Subject (text input, required)
- Category (dropdown, required)
- "Transaksjonsproblem"
- "Tilgangsproblem"
- "Verifisering"
- "Generelt"
- "Tvist"
- Transaction (optional, dropdown from user's transactions):
- "Velg transaksjon (valgfritt)"
- Show recent 10 transactions
- Description (textarea, 5 rows, required)
- "Opprett billett" button
Validation:
- Subject: 5-200 chars
- Description: 20-2000 chars
- Show character count below textarea
Success:
- Redirect to /support/tickets/[new_id]
- Toast: "Billett opprettet. Vi svarer så snart som mulig."
6.5 /admin/support — Admin Dashboard
Layout:
- Header: "Support-billetter"
- Filter bar:
- Status dropdown (all, open, in_progress, waiting_user, resolved, closed)
- Priority dropdown (all, urgent, high, normal, low)
- Category dropdown (all, transaction_issue, account_access, verification, general, dispute)
- Sort dropdown (Nyeste, Eldste, Oppdatert sist, Prioritet)
- Ticket table:
- ID (clickable)
- User (email)
- Subject
- Category badge
- Status badge
- Priority badge
- Created
- Updated
Click ticket:
6.6 /admin/support/[id] — Admin Ticket Detail
Layout:
- Ticket header:
- Subject
- User info: email, phone, KYC status
- Status dropdown (editable)
- Priority dropdown (editable)
- "Lagre endringer" button
- Transaction link (if set): "Se transaksjon" button
- Message thread (same as user view)
- Admin reply form:
- Text area (10 rows)
- "Endre status til:" dropdown (optional)
- "Send svar" button
Audit trail (bottom):
- Show ticket_messages with timestamps
- Show status changes from audit_log
7. Integration Points
7.1 Audit Logging
All support actions logged to audit_log table:
| Action | Resource Type | Details |
|---|---|---|
| support.ticket_created | support_ticket | ticket_id, category, priority |
| support.message_added | ticket_message | ticket_id, sender_type |
| support.admin_reply | ticket_message | ticket_id, admin message preview |
| support.ticket_updated | support_ticket | ticket_id, old_status, new_status |
Implementation: Use auditLog() helper from @/lib/middleware.
7.2 Transaction Linking
When user creates ticket with transaction_id:
- Validate transaction exists and belongs to user
- Display transaction summary in ticket detail:
- Type (remittance / qr_payment)
- Amount
- Recipient / Merchant
- Status
- Created date
- Link to /transactions/[id]
7.3 Email Notifications (Future)
Placeholder implementation:
- Create
src/lib/email-support.ts:export async function notifyUserTicketReply(ticketId: string, userId: string, adminMessage: string) { // TODO: Integrate with MCP email (see ~/system/tools/manifest.md) console.log(`[Email] Notify user ${userId} about ticket ${ticketId}`); } - Call from admin reply endpoint
- Move to MCP email when ready
8. File List
8.1 Database Migration
File: src/lib/db.ts
Changes:
- Add
support_ticketsandticket_messagesschemas toSQLITE_SCHEMAconstant - Add same schemas to
PG_SCHEMAconstant (replacedatetime('now')withCURRENT_TIMESTAMP) - No other changes needed
8.2 API Routes
User Endpoints
src/app/api/support/tickets/route.ts— GET (list), POST (create)src/app/api/support/tickets/[id]/route.ts— GET (detail)src/app/api/support/tickets/[id]/messages/route.ts— POST (add message)
Admin Endpoints
src/app/api/admin/support/tickets/route.ts— GET (list all)src/app/api/admin/support/tickets/[id]/route.ts— PATCH (update status/priority)src/app/api/admin/support/tickets/[id]/messages/route.ts— POST (admin reply)
8.3 UI Pages
User Pages
src/app/support/page.tsx— Help center (FAQ + create button)src/app/support/tickets/page.tsx— Ticket listsrc/app/support/tickets/[id]/page.tsx— Conversation viewsrc/app/support/new/page.tsx— Create ticket form
Admin Pages
src/app/admin/support/page.tsx— Admin dashboard (ticket table)src/app/admin/support/[id]/page.tsx— Admin ticket detail
8.4 Components
src/components/support/ticket-card.tsx— Ticket list item (subject, status, date)src/components/support/message-bubble.tsx— Chat message (user/admin differentiation)src/components/support/status-badge.tsx— Status indicator (open, in_progress, resolved, closed)src/components/support/category-badge.tsx— Category indicatorsrc/components/support/priority-badge.tsx— Priority indicator (urgent, high, normal, low)src/components/support/faq-accordion.tsx— FAQ section (static hardcoded questions)
8.5 Types
File: src/types/support.ts
export interface SupportTicket {
id: string;
user_id: string;
subject: string;
description: string;
category: TicketCategory;
status: TicketStatus;
priority: TicketPriority;
transaction_id: string | null;
created_at: string;
updated_at: string;
resolved_at: string | null;
closed_at: string | null;
}
export interface TicketMessage {
id: string;
ticket_id: string;
sender_type: 'user' | 'admin';
sender_id: string | null;
message: string;
created_at: string;
}
export type TicketCategory = 'transaction_issue' | 'account_access' | 'verification' | 'general' | 'dispute';
export type TicketStatus = 'open' | 'in_progress' | 'waiting_user' | 'resolved' | 'closed';
export type TicketPriority = 'low' | 'normal' | 'high' | 'urgent';
8.6 Utilities
File: src/lib/support-utils.ts
import { TicketCategory, TicketPriority } from '@/types/support';
export function getAutoPriority(category: TicketCategory): TicketPriority {
if (category === 'dispute') return 'high';
if (category === 'account_access') return 'high';
if (category === 'transaction_issue') return 'normal';
if (category === 'verification') return 'normal';
return 'low'; // general
}
export function getCategoryLabel(category: TicketCategory): string {
const labels: Record<TicketCategory, string> = {
transaction_issue: 'Transaksjonsproblem',
account_access: 'Tilgangsproblem',
verification: 'Verifisering',
general: 'Generelt',
dispute: 'Tvist',
};
return labels[category];
}
export function getStatusLabel(status: TicketStatus): string {
const labels = {
open: 'Åpen',
in_progress: 'Under behandling',
waiting_user: 'Venter på deg',
resolved: 'Løst',
closed: 'Lukket',
};
return labels[status];
}
export function getPriorityLabel(priority: TicketPriority): string {
const labels = {
urgent: 'Hastesak',
high: 'Høy',
normal: 'Normal',
low: 'Lav',
};
return labels[priority];
}
9. Acceptance Criteria
9.1 Database
- support_tickets table created with all fields and constraints
- ticket_messages table created with foreign key to support_tickets
- Indexes created for performance (user_id, status, category, created_at, transaction_id)
- Schema works for both SQLite (dev) and PostgreSQL (staging)
9.2 API Endpoints
- POST /api/support/tickets creates ticket + initial message
- GET /api/support/tickets returns user's tickets with pagination
- GET /api/support/tickets/[id] returns ticket + messages
- POST /api/support/tickets/[id]/messages adds user message
- GET /api/admin/support/tickets returns all tickets (admin only)
- PATCH /api/admin/support/tickets/[id] updates status/priority (admin only)
- POST /api/admin/support/tickets/[id]/messages adds admin reply + sends email notification
9.3 Authorization
- Users can only view their own tickets (404 on unauthorized access)
- Admin endpoints require admin role (403 if not admin)
- CSRF protection via validateOrigin() middleware
9.4 UI Pages
- /support shows FAQ + create button
- /support/tickets shows user's tickets with status filter
- /support/tickets/[id] shows conversation thread + reply form
- /support/new shows create form with validation
- /admin/support shows all tickets with filters (status, priority, category)
- /admin/support/[id] shows ticket detail + admin reply form
9.5 Integration
- All support actions logged to audit_log
- Tickets can be linked to transactions
- Transaction summary displayed in ticket detail (if linked)
- Admin reply triggers email notification (placeholder implementation)
9.6 Validation
- Subject: 5-200 chars, sanitized
- Description: 20-2000 chars, sanitized
- Category: must be valid enum value
- Transaction ID: must exist and belong to user (if provided)
- Status transitions validated (open → in_progress → resolved → closed)
9.7 Edge Cases
- Closed tickets cannot receive new messages (UI shows message)
- Reopening resolved ticket changes status back to 'open'
- Empty ticket list shows "Ingen billetter" message
- Character count shown in textarea (5-200 for subject, 20-2000 for description)
10. Dependencies
10.1 Existing Infrastructure
- Database:
src/lib/db.ts(SQLite/PostgreSQL dual driver) - Auth:
src/lib/auth.ts(getCurrentUser, JWT validation) - Middleware:
src/lib/middleware.ts(requireAuth, sanitizeText, auditLog) - Utils:
src/lib/utils-server.ts(randomId)
10.2 New Dependencies
None. All features use existing infrastructure.
10.3 Admin Role (TBD)
Current state: Users table has role field with values user or merchant.
Requirement: Add admin role for support staff.
Two options:
Option A: Extend existing role enum
ALTER TABLE users DROP CONSTRAINT users_role_check;
ALTER TABLE users ADD CONSTRAINT users_role_check CHECK(role IN ('user','merchant','admin'));
Option B: Separate admin table
CREATE TABLE IF NOT EXISTS admins (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
name TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
Recommendation: Option A (extend role enum) for MVP. Simpler, uses existing auth.
Implementation:
- Add migration to extend role enum
- Create
requireAdmin()middleware:export async function requireAdmin(request?: NextRequest) { const { user, error } = await requireAuth(request); if (error) return { user: null, error }; if ((user as Record<string, unknown>).role !== 'admin') { return { user: null, error: jsonError('forbidden', 'Admin role required', 403) }; } return { user, error: null }; } - Use in all admin endpoints
11. Testing Checklist
11.1 Unit Tests (Future)
- getAutoPriority() returns correct priority for each category
- getCategoryLabel() returns Norwegian labels
- Status validation logic
11.2 Integration Tests (Future)
- Create ticket → appears in user's ticket list
- Admin reply → user sees message in conversation
- Status change → audit log entry created
- Transaction link → transaction summary displayed
11.3 Manual Testing (MVP)
-
User flow:
- Create ticket with subject, description, category
- View ticket list (should show new ticket)
- Open ticket detail (should show initial message)
- Add reply message
- Verify status changes to 'open' if was 'waiting_user'
-
Admin flow:
- View all tickets dashboard
- Filter by status, priority, category
- Open ticket detail
- Change status to 'in_progress'
- Add admin reply
- Verify user sees reply in conversation
-
Authorization:
- User A cannot view User B's tickets (404)
- Non-admin cannot access /admin/support (403)
-
Transaction linking:
- Create ticket linked to transaction
- Verify transaction summary shows in ticket detail
12. Implementation Order
Phase 1: Database + Types (Day 1)
- Add schemas to
src/lib/db.ts - Create
src/types/support.ts - Create
src/lib/support-utils.ts - Add
requireAdmin()tosrc/lib/middleware.ts
Phase 2: User API (Day 1-2)
- POST /api/support/tickets (create)
- GET /api/support/tickets (list)
- GET /api/support/tickets/[id] (detail)
- POST /api/support/tickets/[id]/messages (reply)
Phase 3: User UI (Day 2-3)
- /support (help center)
- /support/new (create form)
- /support/tickets (list)
- /support/tickets/[id] (conversation)
- Components: ticket-card, message-bubble, status-badge, category-badge, priority-badge, faq-accordion
Phase 4: Admin API (Day 3)
- GET /api/admin/support/tickets (list all)
- PATCH /api/admin/support/tickets/[id] (update)
- POST /api/admin/support/tickets/[id]/messages (admin reply)
Phase 5: Admin UI (Day 4)
- /admin/support (dashboard)
- /admin/support/[id] (detail)
Phase 6: Integration (Day 4-5)
- Audit logging for all actions
- Transaction linking
- Email notification (placeholder)
Phase 7: Testing + Refinement (Day 5)
- Manual testing of all flows
- Edge case validation
- UI polish (spacing, colors, responsive)
13. Future Enhancements (Out of Scope)
- File attachments — allow users to upload screenshots
- Multi-language support — English, Bosnian translations
- Public FAQ system — CMS-backed knowledge base
- Live chat — real-time messaging via WebSocket
- AI chatbot — auto-respond to common questions
- SLA tracking — response time targets per priority
- Auto-close old tickets — after 7 days in 'resolved' status
- Admin assignment — assign tickets to specific support staff
- Internal notes — admin-only notes not visible to user
- Ticket templates — pre-filled forms for common issues
14. Notes for Implementation
14.1 Design Consistency
- Match existing Drop UI patterns (see mockups/figma-make-export/)
- Use green accent (#00E5A0) for primary actions
- Dark theme (#09090b background)
- Ensure mobile responsive (all pages must work on 375px width)
14.2 Security
- All text inputs sanitized via
sanitizeText()from middleware - CSRF protection via
validateOrigin()middleware - Audit logging for all support actions
- Users cannot view other users' tickets (authorization check)
14.3 Performance
- Pagination on all list endpoints (default 10, max 50)
- Indexes on user_id, status, category, created_at for fast queries
- Lazy load transaction details only when needed
14.4 Norwegian Text
All UI text in Norwegian (nb):
- "Opprett billett" (Create ticket)
- "Mine support-billetter" (My support tickets)
- "Trenger du hjelp?" (Need help?)
- "Send svar" (Send reply)
- "Under behandling" (In progress)
- "Venter på deg" (Waiting for you)
15. Spec Version History
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2026-02-17 | Initial spec — database, API, UI, integration |
END OF SPEC
No comments to display
No comments to display