Show HN: Hyper, the self driving company brain
bootstrappedHyper — Spécification Technique MVP
1. Vue d'ensemble
Concept : Hyper est un SaaS B2B qui centralise toute la connaissance d'une entreprise (Notion, Slack, Google Drive, emails, réunions) dans un cerveau d'entreprise unifié, interrogeable en langage naturel via un agent IA agentique qui peut répondre, résumer et créer des tâches automatiquement. Valeur ajoutée principale : Zéro friction pour retrouver une information — l'IA cherche, synthétise et agit à la place du collaborateur, capitalisant la connaissance collective même après les départs. Ce qu'on build en MVP (scope réaliste 2-4 semaines) :- Authentification + onboarding équipe
- Connexion à 2 sources max : Notion (OAuth) + upload de fichiers (PDF, DOCX, TXT)
- Pipeline d'indexation : chunking → embeddings → stockage vectoriel (pgvector)
- Interface de chat IA avec RAG (Retrieval-Augmented Generation) + citations de sources
- Agent simple : réponse avec sources + résumé de document à la demande
- Gestion d'équipe (invitations membres) + contrôle d'accès par workspace
- Billing Stripe (trial 14j → plan payant)
- Landing page + emails transactionnels
2. Stack technique
Core
| Couche | Technologie |
|---|---|
| Framework | Next.js 15 (App Router) + TypeScript strict |
| Backend / Auth / DB | Supabase (PostgreSQL + pgvector + Auth + Storage) |
| Billing | Stripe (Checkout + Customer Portal + Webhooks) |
| Emails | Resend + React Email |
| IA / LLM | OpenAI API (GPT-4o pour chat, text-embedding-3-small pour embeddings) |
| Vector Store | pgvector (extension Supabase) |
Dépendances NPM spécifiques
{
"dependencies": {
"@supabase/supabase-js": "^2.45.0",
"@supabase/ssr": "^0.5.0",
"openai": "^4.67.0",
"ai": "^3.4.0",
"stripe": "^17.0.0",
"@stripe/stripe-js": "^4.0.0",
"resend": "^4.0.0",
"@react-email/components": "^0.0.25",
"notion-client": "^6.16.0",
"@notionhq/client": "^2.2.15",
"pdf-parse": "^1.1.1",
"mammoth": "^1.8.0",
"langchain": "^0.3.0",
"@langchain/openai": "^0.3.0",
"@langchain/community": "^0.3.0",
"zod": "^3.23.0",
"zustand": "^5.0.0",
"react-dropzone": "^14.3.0",
"react-markdown": "^9.0.0",
"remark-gfm": "^4.0.0",
"react-syntax-highlighter": "^15.6.0",
"date-fns": "^4.0.0",
"nanoid": "^5.0.0",
"sharp": "^0.33.0",
"cheerio": "^1.0.0",
"p-limit": "^6.1.0",
"tiktoken": "^1.0.17"
},
"devDependencies": {
"tailwindcss": "^3.4.0",
"@tailwindcss/typography": "^0.5.0",
"shadcn-ui": "latest",
"lucide-react": "^0.460.0"
}
}
UI
- shadcn/ui (composants accessibles, thème custom)
- Tailwind CSS v3
- Lucide React (icônes)
3. Modèle de données Supabase
Extensions requises
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";CREATE EXTENSION IF NOT EXISTS "vector";
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- recherche texte floue
Table : workspaces
CREATE TABLE workspaces (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
logo_url TEXT,
owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
plan TEXT NOT NULL DEFAULT 'trial' CHECK (plan IN ('trial', 'starter', 'pro', 'enterprise')),
trial_ends_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() + INTERVAL '14 days'),
stripe_customer_id TEXT UNIQUE,
stripe_subscription_id TEXT UNIQUE,
max_members INT NOT NULL DEFAULT 3,
max_documents INT NOT NULL DEFAULT 100,
settings JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_workspaces_owner ON workspaces(owner_id);
CREATE INDEX idx_workspaces_slug ON workspaces(slug);
CREATE INDEX idx_workspaces_stripe_customer ON workspaces(stripe_customer_id);
Table : workspace_members
CREATE TABLE workspace_members (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
invited_by UUID REFERENCES auth.users(id),
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(workspace_id, user_id)
);
CREATE INDEX idx_wm_workspace ON workspace_members(workspace_id);
CREATE INDEX idx_wm_user ON workspace_members(user_id);
Table : integrations
CREATE TABLE integrations (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
provider TEXT NOT NULL CHECK (provider IN ('notion', 'google_drive', 'slack', 'confluence', 'upload')),
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'error', 'paused', 'disconnected')),
access_token TEXT, -- chiffré avec pgcrypto en prod
refresh_token TEXT, -- chiffré
token_expires_at TIMESTAMPTZ,
external_id TEXT, -- ID workspace/team chez le provider
metadata JSONB NOT NULL DEFAULT '{}', -- config spécifique provider
last_sync_at TIMESTAMPTZ,
sync_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(workspace_id, provider)
);
CREATE INDEX idx_integrations_workspace ON integrations(workspace_id);
CREATE INDEX idx_integrations_provider ON integrations(provider);
Table : documents
CREATE TABLE documents (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
integration_id UUID REFERENCES integrations(id) ON DELETE SET NULL,
title TEXT NOT NULL,
source_type TEXT NOT NULL CHECK (source_type IN ('notion', 'google_drive', 'slack', 'upload', 'confluence', 'manual')),
source_url TEXT,
external_id TEXT, -- ID du doc chez le provider
file_path TEXT, -- chemin Supabase Storage si upload
mime_type TEXT,
raw_content TEXT, -- texte extrait brut (stocké pour re-indexation)
summary TEXT, -- résumé généré par IA
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'indexed', 'error')),
index_error TEXT,
word_count INT DEFAULT 0,
chunk_count INT DEFAULT 0,
metadata JSONB NOT NULL DEFAULT '{}',
indexed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_documents_workspace ON documents(workspace_id);
CREATE INDEX idx_documents_status ON documents(status);
CREATE INDEX idx_documents_source_type ON documents(source_type);
CREATE INDEX idx_documents_external_id ON documents(external_id);
-- Recherche full-text sur le titre
CREATE INDEX idx_documents_title_trgm ON documents USING gin(title gin_trgm_ops);
Table : document_chunks
CREATE TABLE document_chunks (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
chunk_index INT NOT NULL,
content TEXT NOT NULL,
token_count INT NOT NULL DEFAULT 0,
embedding VECTOR(1536), -- text-embedding-3-small = 1536 dims
metadata JSONB NOT NULL DEFAULT '{}', -- page, heading, section, etc.
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Index HNSW pour recherche vectorielle (pgvector)
CREATE INDEX idx_chunks_embedding ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
CREATE INDEX idx_chunks_document ON document_chunks(document_id);
CREATE INDEX idx_chunks_workspace ON document_chunks(workspace_id);
-- Full-text search sur le contenu
CREATE INDEX idx_chunks_content_trgm ON document_chunks USING gin(content gin_trgm_ops);
Table : conversations
CREATE TABLE conversations (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
title TEXT, -- généré automatiquement depuis le 1er message
is_pinned BOOLEAN NOT NULL DEFAULT false,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_conversations_workspace ON conversations(workspace_id);
CREATE INDEX idx_conversations_user ON conversations(user_id);
Table : messages
CREATE TABLE messages (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
content TEXT NOT NULL,
sources JSONB DEFAULT '[]', -- [{chunk_id, document_id, title, url, score}]
tokens_used INT DEFAULT 0,
model TEXT DEFAULT 'gpt-4o',
latency_ms INT,
feedback TEXT CHECK (feedback IN ('positive', 'negative', NULL)),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_messages_conversation ON messages(conversation_id);
CREATE INDEX idx_messages_workspace ON messages(workspace_id);
CREATE INDEX idx_messages_created ON messages(created_at DESC);
Table : invitations
CREATE TABLE invitations (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
email TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('admin', 'member', 'viewer')),
token TEXT NOT NULL UNIQUE DEFAULT encode(gen_random_bytes(32), 'hex'),
invited_by UUID NOT NULL REFERENCES auth.users(id),
accepted_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() + INTERVAL '7 days'),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_invitations_workspace ON invitations(workspace_id);
CREATE INDEX idx_invitations_email ON invitations(email);
CREATE INDEX idx_invitations_token ON invitations(token);
Table : sync_jobs
CREATE TABLE sync_jobs (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
integration_id UUID NOT NULL REFERENCES integrations(id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'completed', 'failed')),
triggered_by TEXT NOT NULL DEFAULT 'manual' CHECK (triggered_by IN ('manual', 'scheduled', 'webhook')),
documents_found INT DEFAULT 0,
documents_indexed INT DEFAULT 0,
documents_failed INT DEFAULT 0,
error_log TEXT,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_sync_jobs_workspace ON sync_jobs(workspace_id);
CREATE INDEX idx_sync_jobs_integration ON sync_jobs(integration_id);
CREATE INDEX idx_sync_jobs_status ON sync_jobs(status);
Table : usage_events
CREATE TABLE usage_events (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id),
event_type TEXT NOT NULL, -- 'query', 'document_indexed', 'sync', 'member_invited'
metadata JSONB NOT NULL DEFAULT '{}',
tokens_used INT DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_usage_workspace ON usage_events(workspace_id);
CREATE INDEX idx_usage_type ON usage_events(event_type);
CREATE INDEX idx_usage_created ON usage_events(created_at DESC);
Table : profiles (existante — extended)
-- Étendre la table profiles existanteALTER TABLE profiles ADD COLUMN IF NOT EXISTS full_name TEXT;
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS avatar_url TEXT;
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS current_workspace_id UUID REFERENCES workspaces(id);
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS onboarding_completed BOOLEAN DEFAULT false;
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS onboarding_step TEXT DEFAULT 'workspace';
RLS Policies
-- ============================================================-- WORKSPACES
-- ============================================================
ALTER TABLE workspaces ENABLE ROW LEVEL SECURITY;
CREATE POLICY "workspace_select" ON workspaces FOR SELECT
USING (
owner_id = auth.uid() OR
id IN (SELECT workspace_id FROM workspace_members WHERE user_id = auth.uid())
);
CREATE POLICY "workspace_insert" ON workspaces FOR INSERT
WITH CHECK (owner_id = auth.uid());
CREATE POLICY "workspace_update" ON workspaces FOR UPDATE
USING (
owner_id = auth.uid() OR
id IN (SELECT workspace_id FROM workspace_members WHERE user_id = auth.uid() AND role IN ('owner', 'admin'))
);
CREATE POLICY "workspace_delete" ON workspaces FOR DELETE
USING (owner_id = auth.uid());
-- ============================================================
-- WORKSPACE_MEMBERS
-- ============================================================
ALTER TABLE workspace_members ENABLE ROW LEVEL SECURITY;
CREATE POLICY "wm_select" ON workspace_members FOR SELECT
USING (
workspace_id IN (SELECT workspace_id FROM workspace_members WHERE user_id = auth.uid())
);
CREATE POLICY "wm_insert" ON workspace_members FOR INSERT
WITH CHECK (
workspace_id IN (
SELECT workspace_id FROM workspace_members
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
)
);
CREATE POLICY "wm_delete" ON workspace_members FOR DELETE
USING (
user_id = auth.uid() OR -- quitter le workspace
workspace_id IN (
SELECT workspace_id FROM workspace_members
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
)
);
-- ============================================================
-- DOCUMENTS
-- ============================================================
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY "documents_workspace_member" ON documents FOR ALL
USING (
workspace_id IN (SELECT workspace_id FROM workspace_members WHERE user_id = auth.uid())
);
-- ============================================================
-- DOCUMENT_CHUNKS
-- ============================================================
ALTER TABLE document_chunks ENABLE ROW LEVEL SECURITY;
CREATE POLICY "chunks_workspace_member" ON document_chunks FOR ALL
USING (
workspace_id IN (SELECT workspace_id FROM workspace_members WHERE user_id = auth.uid())
);
-- ============================================================
-- CONVERSATIONS & MESSAGES
-- ============================================================
ALTER TABLE conversations ENABLE ROW LEVEL SECURITY;
CREATE POLICY "conversations_owner" ON conversations FOR ALL
USING (user_id = auth.uid());
-- Les admins du workspace peuvent voir toutes les conversations
CREATE POLICY "conversations_admin" ON conversations FOR SELECT
USING (
workspace_id IN (
SELECT workspace_id FROM workspace_members
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
)
);
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "messages_via_conversation" ON messages FOR ALL
USING (
conversation_id IN (SELECT id FROM conversations WHERE user_id = auth.uid())
OR
workspace_id IN (
SELECT workspace_id FROM workspace_members
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
)
);
-- ============================================================
-- INTEGRATIONS
-- ============================================================
ALTER TABLE integrations ENABLE ROW LEVEL SECURITY;
CREATE POLICY "integrations_admin" ON integrations FOR ALL
USING (
workspace_id IN (
SELECT workspace_id FROM workspace_members
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
)
);
-- ============================================================
-- INVITATIONS
-- ============================================================
ALTER TABLE invitations ENABLE ROW LEVEL SECURITY;
CREATE POLICY "invitations_manage" ON invitations FOR ALL
USING (
workspace_id IN (
SELECT workspace_id FROM workspace_members
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
)
);
-- ============================================================
-- SYNC_JOBS
-- ============================================================
ALTER TABLE sync_jobs ENABLE ROW LEVEL SECURITY;
CREATE POLICY "sync_jobs_member" ON sync_jobs FOR SELECT
USING (
workspace_id IN (SELECT workspace_id FROM workspace_members WHERE user_id = auth.uid())
);
CREATE POLICY "sync_jobs_admin_write" ON sync_jobs FOR INSERT
WITH CHECK (
workspace_id IN (
SELECT workspace_id FROM workspace_members
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
)
);
Fonction utilitaire : Recherche vectorielle
CREATE OR REPLACE FUNCTION search_documents(query_embedding VECTOR(1536),
workspace_uuid UUID,
match_threshold FLOAT DEFAULT 0.75,
match_count INT DEFAULT 10
)
RETURNS TABLE (
chunk_id UUID,
document_id UUID,
content TEXT,
similarity FLOAT,
doc_title TEXT,
doc_source TEXT,
source_url TEXT,
metadata JSONB
)
LANGUAGE sql STABLE AS $$
SELECT
dc.id AS chunk_id,
dc.document_id AS document_id,
dc.content AS content,
1 - (dc.embedding <=> query_embedding) AS similarity,
d.title AS doc_title,
d.source_type AS doc_source,
d.source_url AS source_url,
dc.metadata AS metadata
FROM document_chunks dc
JOIN documents d ON d.id = dc.document_id
WHERE
dc.workspace_id = workspace_uuid
AND d.status = 'indexed'
AND 1 - (dc.embedding <=> query_embedding) > match_threshold
ORDER BY dc.embedding <=> query_embedding
LIMIT match_count;
$$;
4. Routes API
Authentication & Onboarding
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| POST | /api/auth/callback | Callback OAuth Supabase (magic link, Google) | Non |
| POST | /api/onboarding/workspace | Créer workspace + slug + rôle owner | Oui |
| GET | /api/onboarding/status | Retourner l'étape onboarding courante | Oui |
Workspace & Membres
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| GET | /api/workspace | Infos workspace courant | Oui |
| PATCH| /api/workspace | Mettre à jour nom, logo, settings | Admin |
| GET | /api/workspace/members | Lister les membres | Oui |
| DELETE| /api/workspace/members/[userId] | Retirer un membre | Admin |
| PATCH| /api/workspace/members/[userId] | Changer rôle d'un membre | Admin |
| POST | /api/workspace/invite | Envoyer invitation email | Admin |
| GET | /api/workspace/invite/[token] | Vérifier token invitation | Non |
| POST | /api/workspace/invite/[token]/accept | Accepter invitation | Oui |
Intégrations
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| GET | /api/integrations | Lister toutes les intégrations du workspace | Admin |
| GET | /api/integrations/notion/auth | Initier OAuth Notion → redirect | Admin |
| GET | /api/integrations/notion/callback | Callback OAuth Notion, stocker token | Admin |
| DELETE| /api/integrations/[id] | Déconnecter une intégration | Admin |
| POST | `/api/integrations