Show HN: Claude Code for Customer Support
bootstrappedSupportPilot — Spécification Technique MVP
1. Vue d'ensemble
Concept : SupportPilot est un SaaS B2B qui automatise la rédaction des réponses aux tickets support en s'appuyant sur la base de connaissance de l'entreprise cliente. Un agent IA génère une réponse contextualisée pour chaque nouveau ticket, qu'un humain valide (ou envoie directement) en un clic. Valeur ajoutée principale : Réduire de 80% le temps de traitement des tickets répétitifs pour les équipes support de e-commerces et SaaS SMB français, sans chatbot rigide ni configuration complexe. Ce qu'on build en MVP (48h) :- Onboarding + ingestion de base de connaissance (FAQ, docs texte)
- Réception de tickets via formulaire web + webhook Zendesk basique
- Génération de réponse IA (Claude API) avec contexte RAG minimal
- Interface de validation agent (voir ticket → voir suggestion → valider/éditer/envoyer)
- Dashboard métriques de base
- Auth + Billing (trial 14j → plans payants)
2. Stack technique
Socle
| Technologie | Usage |
|---|---|
| Next.js 15 App Router + TypeScript | Frontend + API Routes |
| Supabase | Auth, PostgreSQL, Storage (docs), Realtime |
| Stripe | Billing, webhooks, portail client |
| Resend | Emails transactionnels |
| Claude API (Anthropic) | Génération des réponses IA |
| OpenAI text-embedding-3-small | Embeddings pour RAG (vecteurs) |
Dépendances NPM spécifiques
{
"@anthropic-ai/sdk": "^0.24.0",
"openai": "^4.47.0",
"@supabase/supabase-js": "^2.43.0",
"@supabase/ssr": "^0.4.0",
"stripe": "^15.12.0",
"@stripe/stripe-js": "^3.5.0",
"resend": "^3.2.0",
"react-email": "^2.1.0",
"@emaildotcom/components": "latest",
"ai": "^3.2.0",
"langchain": "^0.2.0",
"@langchain/community": "^0.2.0",
"pdf-parse": "^1.1.1",
"mammoth": "^1.7.2",
"zod": "^3.23.0",
"react-hook-form": "^7.51.0",
"@hookform/resolvers": "^3.4.0",
"sonner": "^1.5.0",
"zustand": "^4.5.0",
"date-fns": "^3.6.0",
"recharts": "^2.12.0",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-badge": "^1.0.0",
"lucide-react": "^0.390.0",
"tailwind-merge": "^2.3.0",
"clsx": "^2.1.1",
"next-themes": "^0.3.0",
"sharp": "^0.33.4"
}
> Note architecture RAG : On utilise pgvector (extension Supabase) pour stocker et requêter les embeddings directement en PostgreSQL. Pas de Pinecone/Weaviate pour le MVP.
3. Modèle de données Supabase
Extensions requises
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";CREATE EXTENSION IF NOT EXISTS "vector";
Table : organizations
CREATE TABLE organizations (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL, -- ex: "mon-eshop"
plan TEXT NOT NULL DEFAULT 'trial' -- 'trial' | 'starter' | 'pro' | 'scale'
CHECK (plan IN ('trial', 'starter', 'pro', 'scale')),
trial_ends_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '14 days',
stripe_customer_id TEXT UNIQUE,
stripe_subscription_id TEXT UNIQUE,
subscription_status TEXT DEFAULT 'trialing'
CHECK (subscription_status IN ('trialing', 'active', 'past_due', 'canceled', 'unpaid')),
tickets_used_this_month INT NOT NULL DEFAULT 0,
tickets_limit INT NOT NULL DEFAULT 100, -- selon le plan
settings JSONB NOT NULL DEFAULT '{}', -- auto_send, tone, language...
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- RLS
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
CREATE POLICY "org_members_can_read" ON organizations
FOR SELECT USING (
id IN (
SELECT organization_id FROM profiles WHERE id = auth.uid()
)
);
CREATE POLICY "org_members_can_update" ON organizations
FOR UPDATE USING (
id IN (
SELECT organization_id FROM profiles WHERE id = auth.uid()
)
);
-- Index
CREATE INDEX idx_organizations_slug ON organizations(slug);
CREATE INDEX idx_organizations_stripe_customer ON organizations(stripe_customer_id);
Table : profiles (étend auth.users)
CREATE TABLE profiles (id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
organization_id UUID REFERENCES organizations(id) ON DELETE SET NULL,
email TEXT NOT NULL,
full_name TEXT,
avatar_url TEXT,
role TEXT NOT NULL DEFAULT 'agent'
CHECK (role IN ('owner', 'admin', 'agent')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "profiles_self_read" ON profiles
FOR SELECT USING (id = auth.uid());
CREATE POLICY "profiles_org_read" ON profiles
FOR SELECT USING (
organization_id IN (
SELECT organization_id FROM profiles WHERE id = auth.uid()
)
);
CREATE POLICY "profiles_self_update" ON profiles
FOR UPDATE USING (id = auth.uid());
CREATE INDEX idx_profiles_organization ON profiles(organization_id);
Table : knowledge_sources
> Fichiers/URLs ingérés comme base de connaissance
CREATE TABLE knowledge_sources (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('file', 'url', 'manual', 'zendesk_macro')),
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'processing', 'ready', 'error')),
file_path TEXT, -- chemin Supabase Storage
url TEXT, -- si type = 'url'
raw_content TEXT, -- si type = 'manual'
file_size_bytes INT,
chunks_count INT DEFAULT 0,
error_message TEXT,
metadata JSONB DEFAULT '{}',
created_by UUID REFERENCES profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE knowledge_sources ENABLE ROW LEVEL SECURITY;
CREATE POLICY "ks_org_members" ON knowledge_sources
FOR ALL USING (
organization_id IN (
SELECT organization_id FROM profiles WHERE id = auth.uid()
)
);
CREATE INDEX idx_ks_organization ON knowledge_sources(organization_id);
CREATE INDEX idx_ks_status ON knowledge_sources(status);
Table : knowledge_chunks
> Chunks vectorisés pour le RAG
CREATE TABLE knowledge_chunks (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
source_id UUID NOT NULL REFERENCES knowledge_sources(id) ON DELETE CASCADE,
content TEXT NOT NULL,
embedding VECTOR(1536), -- OpenAI text-embedding-3-small = 1536 dims
chunk_index INT NOT NULL,
metadata JSONB DEFAULT '{}', -- ex: {"page": 1, "section": "Livraison"}
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE knowledge_chunks ENABLE ROW LEVEL SECURITY;
CREATE POLICY "kc_org_members" ON knowledge_chunks
FOR SELECT USING (
organization_id IN (
SELECT organization_id FROM profiles WHERE id = auth.uid()
)
);
-- Index HNSW pour similarity search rapide
CREATE INDEX idx_kc_embedding ON knowledge_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
CREATE INDEX idx_kc_organization ON knowledge_chunks(organization_id);
CREATE INDEX idx_kc_source ON knowledge_chunks(source_id);
Table : tickets
CREATE TABLE tickets (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
external_id TEXT, -- ID Zendesk/Intercom si webhook
source TEXT NOT NULL DEFAULT 'web'
CHECK (source IN ('web', 'zendesk', 'email', 'api')),
status TEXT NOT NULL DEFAULT 'pending_ai'
CHECK (status IN ('pending_ai', 'pending_review', 'approved', 'sent', 'rejected', 'escalated')),
priority TEXT DEFAULT 'normal'
CHECK (priority IN ('low', 'normal', 'high', 'urgent')),
-- Données client
customer_name TEXT,
customer_email TEXT,
customer_id TEXT, -- ID externe (ex: ID commande Shopify)
-- Contenu
subject TEXT,
body TEXT NOT NULL,
attachments JSONB DEFAULT '[]', -- URLs fichiers joints
-- IA
ai_response TEXT, -- Réponse générée
ai_confidence FLOAT, -- Score confiance 0-1
ai_context_used JSONB DEFAULT '[]', -- Chunks RAG utilisés
ai_generated_at TIMESTAMPTZ,
ai_tokens_used INT DEFAULT 0,
-- Traitement humain
assigned_to UUID REFERENCES profiles(id) ON DELETE SET NULL,
reviewed_by UUID REFERENCES profiles(id) ON DELETE SET NULL,
final_response TEXT, -- Réponse finale (validée ou éditée)
sent_at TIMESTAMPTZ,
review_note TEXT, -- Note interne agent
-- Métriques
first_response_time_sec INT, -- Délai ticket créé → réponse envoyée
was_edited BOOLEAN DEFAULT FALSE, -- L'IA a-t-elle été corrigée ?
satisfaction_score INT -- 1-5 si feedback client activé
CHECK (satisfaction_score BETWEEN 1 AND 5),
tags TEXT[] DEFAULT '{}',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE tickets ENABLE ROW LEVEL SECURITY;
CREATE POLICY "tickets_org_members" ON tickets
FOR ALL USING (
organization_id IN (
SELECT organization_id FROM profiles WHERE id = auth.uid()
)
);
CREATE INDEX idx_tickets_organization ON tickets(organization_id);
CREATE INDEX idx_tickets_status ON tickets(status);
CREATE INDEX idx_tickets_created_at ON tickets(created_at DESC);
CREATE INDEX idx_tickets_assigned_to ON tickets(assigned_to);
CREATE INDEX idx_tickets_customer_email ON tickets(customer_email);
CREATE INDEX idx_tickets_external_id ON tickets(external_id);
Table : ticket_events
> Audit trail / historique des actions sur un ticket
CREATE TABLE ticket_events (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
ticket_id UUID NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
actor_id UUID REFERENCES profiles(id) ON DELETE SET NULL,
event_type TEXT NOT NULL, -- 'created' | 'ai_generated' | 'approved' | 'edited' | 'sent' | 'escalated' | 'rejected'
payload JSONB DEFAULT '{}', -- données contextuelles
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE ticket_events ENABLE ROW LEVEL SECURITY;
CREATE POLICY "te_org_members" ON ticket_events
FOR SELECT USING (
ticket_id IN (
SELECT id FROM tickets WHERE organization_id IN (
SELECT organization_id FROM profiles WHERE id = auth.uid()
)
)
);
CREATE INDEX idx_te_ticket ON ticket_events(ticket_id);
CREATE INDEX idx_te_created_at ON ticket_events(created_at DESC);
Table : integrations
CREATE TABLE integrations (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
provider TEXT NOT NULL CHECK (provider IN ('zendesk', 'intercom', 'shopify', 'webhook')),
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'error')),
config JSONB NOT NULL DEFAULT '{}', -- credentials chiffrées, subdomain, etc.
webhook_secret TEXT, -- secret pour valider les webhooks entrants
last_sync_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(organization_id, provider)
);
ALTER TABLE integrations ENABLE ROW LEVEL SECURITY;
CREATE POLICY "integrations_org_admin" ON integrations
FOR ALL USING (
organization_id IN (
SELECT organization_id FROM profiles
WHERE id = auth.uid() AND role IN ('owner', 'admin')
)
);
CREATE INDEX idx_integrations_organization ON integrations(organization_id);
Table : subscription_events (déjà existante — rappel du schéma)
CREATE TABLE subscription_events (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID REFERENCES organizations(id) ON DELETE SET NULL,
stripe_event_id TEXT UNIQUE NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Fonction SQL utilitaire : recherche vectorielle
CREATE OR REPLACE FUNCTION match_knowledge_chunks(query_embedding VECTOR(1536),
org_id UUID,
match_threshold FLOAT DEFAULT 0.75,
match_count INT DEFAULT 5
)
RETURNS TABLE (
id UUID,
content TEXT,
metadata JSONB,
similarity FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT
kc.id,
kc.content,
kc.metadata,
1 - (kc.embedding <=> query_embedding) AS similarity
FROM knowledge_chunks kc
WHERE
kc.organization_id = org_id
AND 1 - (kc.embedding <=> query_embedding) > match_threshold
ORDER BY kc.embedding <=> query_embedding
LIMIT match_count;
END;
$$;
4. Routes API
Auth & Onboarding
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| POST | /api/auth/signup | Création compte + org | Non |
| POST | /api/auth/onboarding | Finalise setup org (nom, secteur) | Oui |
Organisation
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| GET | /api/org | Récupère l'org courante + settings | Oui |
| PATCH | /api/org | Met à jour settings org (tone, auto_send, etc.) | Oui (admin) |
| GET | /api/org/usage | Stats usage mois courant (tickets, tokens) | Oui |
Base de connaissance
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| GET | /api/knowledge | Liste des sources | Oui |
| POST | /api/knowledge/upload | Upload fichier (PDF/DOCX/TXT) → Supabase Storage → ingestion async | Oui (admin) |
| POST | /api/knowledge/url | Ajoute une URL à crawler | Oui (admin) |
| POST | /api/knowledge/manual | Ajoute du texte libre | Oui (admin) |
| DELETE | /api/knowledge/[id] | Supprime source + chunks associés | Oui (admin) |
| GET | /api/knowledge/[id]/status | Polling statut ingestion | Oui |
| POST | /api/knowledge/[id]/reprocess | Relance l'ingestion | Oui (admin) |
Ingestion (interne / background)
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| POST | /api/internal/ingest | Worker : parse + chunk + embed + store | Secret interne |
Tickets
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| GET | /api/tickets | Liste tickets (filtrés, paginés) | Oui |
| GET | /api/tickets/[id] | Détail ticket + événements | Oui |
| POST | /api/tickets | Crée ticket manuel (formulaire web public ou interne) | Optionnel |
| PATCH | /api/tickets/[id] | Modifie assignation, tags, priorité | Oui |
| POST | /api/tickets/[id]/generate | (Re)génère réponse IA | Oui |
| POST | /api/tickets/[id]/approve | Valide et envoie la réponse IA | Oui |
| POST | /api/tickets/[id]/edit-send | Modifie la réponse puis envoie | Oui |
| POST | /api/tickets/[id]/reject | Rejette la suggestion IA | Oui |
| POST | /api/tickets/[id]/escalate | Escalade le ticket | Oui |
Webhook entrant (intégrations)
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| POST | /api/webhooks/zendesk | Reçoit nouveau ticket Zendesk | HMAC secret |
| POST | /api/webhooks/generic | Webhook générique (format standard) | API Key header |
Intégrations
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| GET | /api/integrations | Liste des intégrations configurées | Oui (admin) |
| POST | /api/integrations/zendesk | Configure intégration Zendesk | Oui (admin) |
| DELETE | /api/integrations/[id] | Supprime une intégration | Oui (admin) |
Billing
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| POST | /api/billing/create-checkout | Crée session Stripe Checkout | Oui (owner) |
| POST | /api/billing/portal | Génère lien Stripe Customer Portal | Oui (owner) |
| POST | /api/billing/webhook | Stripe webhook (events billing) | Stripe signature |
Analytics
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| GET | /api/analytics/overview | KPIs globaux (taux résolution, MTTR, etc.) | Oui |
| GET | /api/analytics/tickets | Évolution tickets dans le temps | Oui |
Public (ticket form)
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| POST | /api/public/[orgSlug]/ticket | Formulaire public de soumission ticket | Non |
5. Pages & Architecture
Structure des layouts
app/├── (public)/ # Layout minimal (LP, auth)
│ ├── layout.tsx # Header LP + footer
│ ├── page.tsx # Landing page
│ ├── pricing/page.tsx
│ ├── auth/
│ │ ├── login/page.tsx
│ │ ├── signup/page.tsx
│ │ └── callback/page.tsx # OAuth callback Supabase
│
├── (onboarding)/ # Layout centré, stepper
│ ├── layout.tsx
│ └── onboarding/
│ ├── step-1/page.tsx # Nom org + secteur
│ ├── step-2/page.tsx # Upload première source KB
│ └── step-3/page.tsx # Invite membres (optionnel)
│
├── (app)/ # Dashboard principal — auth required
│ ├── layout.tsx # Sidebar + topbar + org context
│ ├── dashboard/page.tsx # Vue d'ensemble / métriques
│ ├── tickets/
│ │ ├── page.tsx # Liste tickets (queue)
│ │ └── [id]/page.tsx # Détail ticket + validation IA
│ ├── knowledge/
│ │ └── page.tsx # Gestion base de connaissance
│ ├── integrations/
│ │ └── page.tsx # Config Zendesk, webhooks
│ ├── settings/
│ │ ├── page.tsx # Settings org (tone, auto-send)
│ │ └── billing/page.tsx # Plan, usage, upgrade
│ └── team/
│ └── page.tsx # Membres + rôles
│
└── (portal)/ # Formulaire public client
└── support/[orgSlug]/page.tsx # Page de contact publique