SaaS / Show HN: Claude Code for Customer Support

Show HN: Claude Code for Customer Support

bootstrapped
L
👥 Voir les clients
MRR
0€/m
Clients payants
0
Trials
0
Churn
0%
Fiche produit
ICP
Responsable e-commerce ou SaaS SMB (5–50 salariés)
Problème résolu
Votre équipe support croule sous les tickets répétitifs et perd des heures à rédiger les mêmes réponses chaque jour
Pricing
SupportPilot
49
Pro
129
Business
299
Trial : 14 jours gratuits
Mettre à jour les métriques
Docs générés automatiquement
SPEC.mdDESIGN.mdMARKETING.md

SupportPilot — 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


Pages dét