diff --git a/.gitignore b/.gitignore index 3d70248ba..635ca404d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ node_modules .env.production.local build +frontend/dist npm-debug.log* yarn-debug.log* diff --git a/BACKEND_WIRING.md b/BACKEND_WIRING.md new file mode 100644 index 000000000..02fa148e6 --- /dev/null +++ b/BACKEND_WIRING.md @@ -0,0 +1,897 @@ +# Backend Wiring Guide — Connect Africa + +## Entrepreneur Registration, Submission & Payment Flow + +This guide walks through every step needed to wire the frontend pages to a live backend. +Stack: **Supabase** (auth, database, storage, realtime) + **Express** backend + **Stripe**. + +--- + +## Table of Contents + +1. [Environment Variables](#1-environment-variables) +2. [Supabase — Database Tables](#2-supabase--database-tables) +3. [Supabase — Row Level Security (RLS)](#3-supabase--row-level-security-rls) +4. [Supabase — Storage Buckets](#4-supabase--storage-buckets) +5. [Supabase — Auth & Email Confirmation](#5-supabase--auth--email-confirmation) +6. [Frontend — Supabase Client](#6-frontend--supabase-client) +7. [Wire: Registration (`Register.jsx`)](#7-wire-registration-registerjsx) +8. [Wire: Welcome / T&C Acceptance (`Welcome.jsx`)](#8-wire-welcome--tc-acceptance-welcomejsx) +9. [Wire: Profile Page (`Profile.jsx`)](#9-wire-profile-page-profilejsx) +10. [Wire: Project Submission (`SubmitProject.jsx`)](#10-wire-project-submission-submitprojectjsx) +11. [Wire: Payment & Verification (`Payment.jsx`)](#11-wire-payment--verification-paymentjsx) +12. [Wire: Dashboard — Submissions & Chat (`Dashboard.jsx`)](#12-wire-dashboard--submissions--chat-dashboardjsx) +13. [Wire: Project Feed (`ProjectFeed.jsx`)](#13-wire-project-feed-projectfeedjsx) +14. [Express Backend Routes](#14-express-backend-routes) +15. [Stripe Setup](#15-stripe-setup) +16. [Deployment Checklist](#16-deployment-checklist) + +--- + +## 1. Environment Variables + +### `backend/.env` + +```env +# Supabase +SUPABASE_URL=https://.supabase.co +SUPABASE_ANON_KEY= +SUPABASE_SERVICE_ROLE_KEY= # Admin ops, bypasses RLS + +# Stripe +STRIPE_SECRET_KEY=sk_live_... # or sk_test_... for development +STRIPE_WEBHOOK_SECRET=whsec_... # from Stripe Dashboard → Webhooks + +# App +FRONTEND_URL=https://your-deployed-frontend.com +PORT=8080 +``` + +### `frontend/.env` (create this file) + +```env +VITE_SUPABASE_URL=https://.supabase.co +VITE_SUPABASE_ANON_KEY= +VITE_API_URL=http://localhost:8080 # your Express backend URL +``` + +> **Never commit `.env` files.** Both are already in `.gitignore`. + +--- + +## 2. Supabase — Database Tables + +Open your Supabase project → **SQL Editor** → run each block below. + +### 2a. `entrepreneurs` table + +```sql +create table public.entrepreneurs ( + id uuid primary key references auth.users(id) on delete cascade, + first_name text not null, + last_name text not null, + email text not null, + phone text not null, + company_name text not null, + company_type text not null check (company_type in ('startup', 'ongoing')), + years_operating integer, + employees text, + website text, + company_address text not null, + city text not null, + country text not null, + avatar_url text, + bio text, + presentation text, + terms_accepted boolean not null default false, + terms_accepted_at timestamptz, + created_at timestamptz default now() +); +``` + +### 2b. `projects` table + +```sql +create table public.projects ( + id uuid primary key default gen_random_uuid(), + entrepreneur_id uuid not null references public.entrepreneurs(id) on delete cascade, + title text not null, + category text not null, + stage text not null, + amount_seeking text not null, + country text not null, + summary text not null, + pitch_url text, -- Supabase Storage path + business_plan_url text, -- Supabase Storage path (optional) + status text not null default 'pending_payment' + check (status in ('pending_payment','under_review','approved','failed_screening')), + views integer not null default 0, + interests integer not null default 0, + submitted_at timestamptz, + created_at timestamptz default now() +); +``` + +### 2c. `payments` table + +```sql +create table public.payments ( + id uuid primary key default gen_random_uuid(), + project_id uuid not null references public.projects(id) on delete cascade, + entrepreneur_id uuid not null references public.entrepreneurs(id), + stripe_payment_intent_id text unique, + amount_cents integer not null default 100000, -- €1,000.00 + currency text not null default 'eur', + status text not null default 'pending' + check (status in ('pending','succeeded','refunded','partially_refunded')), + verification_date date, + verification_time text, -- e.g. "09:30" + created_at timestamptz default now() +); +``` + +### 2d. `messages` table (admin chat) + +```sql +create table public.messages ( + id uuid primary key default gen_random_uuid(), + entrepreneur_id uuid not null references public.entrepreneurs(id) on delete cascade, + sender text not null check (sender in ('user', 'admin')), + sender_name text not null, + text text not null, + created_at timestamptz default now() +); +``` + +### 2e. `project_interests` table + +```sql +create table public.project_interests ( + id uuid primary key default gen_random_uuid(), + project_id uuid not null references public.projects(id) on delete cascade, + user_id uuid not null references auth.users(id) on delete cascade, + created_at timestamptz default now(), + unique(project_id, user_id) +); +``` + +--- + +## 3. Supabase — Row Level Security (RLS) + +Run in SQL Editor. This ensures users can only read/write their own data. + +```sql +-- Enable RLS on all tables +alter table public.entrepreneurs enable row level security; +alter table public.projects enable row level security; +alter table public.payments enable row level security; +alter table public.messages enable row level security; +alter table public.project_interests enable row level security; + +-- entrepreneurs: only the owner can read/write their own row +create policy "owner access" on public.entrepreneurs + for all using (auth.uid() = id); + +-- projects: owner can manage; everyone can read approved ones +create policy "owner manage projects" on public.projects + for all using (auth.uid() = entrepreneur_id); + +create policy "public read approved projects" on public.projects + for select using (status = 'approved'); + +-- payments: owner only +create policy "owner payments" on public.payments + for all using (auth.uid() = entrepreneur_id); + +-- messages: owner reads/writes their own thread +create policy "owner messages" on public.messages + for all using (auth.uid() = entrepreneur_id); + +-- project_interests: authenticated users can insert/read their own +create policy "own interests" on public.project_interests + for all using (auth.uid() = user_id); +``` + +--- + +## 4. Supabase — Storage Buckets + +In the Supabase Dashboard → **Storage** → create two buckets: + +| Bucket name | Public? | Max file size | Allowed MIME types | +|---|---|---|---| +| `avatars` | ✅ Public | 5 MB | `image/jpeg, image/png, image/gif, image/webp` | +| `project-docs` | ❌ Private | 50 MB | `application/pdf` | + +Then add storage policies in SQL Editor: + +```sql +-- avatars: anyone can read; only owner can upload to their own folder +create policy "public read avatars" on storage.objects + for select using (bucket_id = 'avatars'); + +create policy "owner upload avatar" on storage.objects + for insert with check ( + bucket_id = 'avatars' and + auth.uid()::text = (storage.foldername(name))[1] + ); + +-- project-docs: only the owning entrepreneur can upload/read +create policy "owner upload docs" on storage.objects + for insert with check ( + bucket_id = 'project-docs' and + auth.uid()::text = (storage.foldername(name))[1] + ); + +create policy "owner read docs" on storage.objects + for select using ( + bucket_id = 'project-docs' and + auth.uid()::text = (storage.foldername(name))[1] + ); +``` + +--- + +## 5. Supabase — Auth & Email Confirmation + +### 5a. Enable email confirmations + +Dashboard → **Authentication → Providers → Email**: +- Toggle **"Confirm email"** ON +- Set **"Redirect URL"** to: `https://your-frontend.com/welcome` + +### 5b. Customise the confirmation email + +Dashboard → **Authentication → Email Templates → Confirm signup**: + +```html +

Confirm your Connect Africa account

+

Click the button below to confirm your email address and activate your account.

+Confirm Email +

This link expires in 24 hours.

+``` + +The `ConfirmationURL` automatically redirects to your **Redirect URL** after clicking, which should be `/welcome`. + +### 5c. Handle the redirect in `Welcome.jsx` + +When Supabase redirects to `/welcome`, it appends a token to the URL. Supabase JS handles this automatically — the session is set when the page loads. You just need to check the session is valid: + +```js +// At the top of Welcome.jsx — add this useEffect +import { supabase } from "../../lib/supabase"; + +useEffect(() => { + supabase.auth.getSession().then(({ data: { session } }) => { + if (!session) { + // Not confirmed yet — redirect back + navigate("/register"); + } + }); +}, []); +``` + +--- + +## 6. Frontend — Supabase Client + +Create `frontend/src/lib/supabase.js`: + +```js +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; +const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; + +export const supabase = createClient(supabaseUrl, supabaseAnonKey); +``` + +Install the package: + +```bash +cd frontend && npm install @supabase/supabase-js +``` + +--- + +## 7. Wire: Registration (`Register.jsx`) + +Replace the `handleSubmit` function body: + +```js +import { supabase } from "../../lib/supabase"; + +async function handleSubmit(e) { + e.preventDefault(); + const errs = validate(); + if (Object.keys(errs).length > 0) { + setErrors(errs); + window.scrollTo({ top: 0, behavior: "smooth" }); + return; + } + + // 1. Create auth user — Supabase sends confirmation email automatically + const { data: authData, error: authError } = await supabase.auth.signUp({ + email: form.email, + password: form.password, + options: { + emailRedirectTo: `${window.location.origin}/welcome`, + }, + }); + + if (authError) { + setErrors({ email: authError.message }); + return; + } + + // 2. Insert entrepreneur profile row + const { error: profileError } = await supabase.from("entrepreneurs").insert({ + id: authData.user.id, + first_name: form.firstName, + last_name: form.lastName, + email: form.email, + phone: `${form.phoneCode} ${form.phone}`, + company_name: form.companyName, + company_type: form.companyType, + years_operating: form.yearsOperating ? parseInt(form.yearsOperating) : null, + employees: form.employees, + website: form.website || null, + company_address: form.companyAddress, + city: form.city, + country: form.country, + }); + + if (profileError) { + console.error("Profile insert error:", profileError); + // Auth user was created — don't block UX, show confirmation anyway + } + + setSubmitted(true); +} +``` + +--- + +## 8. Wire: Welcome / T&C Acceptance (`Welcome.jsx`) + +Replace `handleAccept`: + +```js +import { supabase } from "../../lib/supabase"; + +async function handleAccept() { + if (!accepted) return; + + const { data: { user } } = await supabase.auth.getUser(); + + const { error } = await supabase + .from("entrepreneurs") + .update({ + terms_accepted: true, + terms_accepted_at: new Date().toISOString(), + }) + .eq("id", user.id); + + if (error) { + console.error("T&C update error:", error); + return; + } + + navigate("/entrepreneur/profile"); +} +``` + +--- + +## 9. Wire: Profile Page (`Profile.jsx`) + +### 9a. Load real data on mount + +```js +import { useEffect } from "react"; +import { supabase } from "../../lib/supabase"; + +const [profile, setProfile] = useState(null); + +useEffect(() => { + async function loadProfile() { + const { data: { user } } = await supabase.auth.getUser(); + const { data } = await supabase + .from("entrepreneurs") + .select("*") + .eq("id", user.id) + .single(); + if (data) { + setProfile(data); + setBio(data.bio || ""); + setPresentation(data.presentation || ""); + setAvatarPreview(data.avatar_url || null); + } + } + loadProfile(); +}, []); +``` + +### 9b. Save profile with avatar upload + +```js +async function handleSave(e) { + e.preventDefault(); + setSaving(true); + + const { data: { user } } = await supabase.auth.getUser(); + let avatarUrl = profile?.avatar_url || null; + + // Upload avatar if a new file was selected + if (avatar) { + const ext = avatar.name.split(".").pop(); + const path = `${user.id}/avatar.${ext}`; + const { error: uploadError } = await supabase.storage + .from("avatars") + .upload(path, avatar, { upsert: true }); + + if (!uploadError) { + const { data: urlData } = supabase.storage + .from("avatars") + .getPublicUrl(path); + avatarUrl = urlData.publicUrl; + } + } + + await supabase + .from("entrepreneurs") + .update({ bio, presentation, avatar_url: avatarUrl }) + .eq("id", user.id); + + setSaving(false); + setSaved(true); +} +``` + +--- + +## 10. Wire: Project Submission (`SubmitProject.jsx`) + +Replace `handleSubmit` — upload PDFs to Supabase Storage then insert project row: + +```js +import { supabase } from "../../lib/supabase"; + +async function handleSubmit(e) { + e.preventDefault(); + const errs = validate(); + if (Object.keys(errs).length > 0) { + setErrors(errs); + window.scrollTo({ top: 0, behavior: "smooth" }); + return; + } + + const { data: { user } } = await supabase.auth.getUser(); + + // Upload pitch PDF + const pitchPath = `${user.id}/${Date.now()}-pitch.pdf`; + const { error: pitchError } = await supabase.storage + .from("project-docs") + .upload(pitchPath, form.pitchFile); + + if (pitchError) { + setErrors({ pitchFile: "Upload failed. Please try again." }); + return; + } + + // Upload business plan PDF (optional) + let bpPath = null; + if (form.businessPlanFile) { + bpPath = `${user.id}/${Date.now()}-business-plan.pdf`; + await supabase.storage.from("project-docs").upload(bpPath, form.businessPlanFile); + } + + // Insert project row (status = pending_payment until Stripe confirms) + const { data: project, error: insertError } = await supabase + .from("projects") + .insert({ + entrepreneur_id: user.id, + title: form.title, + category: form.category, + stage: form.stage, + amount_seeking: form.amountSeeking, + country: form.country, + summary: form.summary, + pitch_url: pitchPath, + business_plan_url: bpPath, + status: "pending_payment", + }) + .select() + .single(); + + if (insertError) { + setErrors({ title: "Submission failed. Please try again." }); + return; + } + + // Pass project ID to payment page + navigate("/entrepreneur/payment", { + state: { project: { ...form, id: project.id } }, + }); +} +``` + +--- + +## 11. Wire: Payment & Verification (`Payment.jsx`) + +Payment requires a backend API call to Stripe. This is a **two-step** process. + +### 11a. Create a Stripe Payment Intent (backend) + +Add this route to `backend/server.js`: + +```js +import Stripe from "stripe"; +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + +// POST /api/payments/create-intent +app.post("/api/payments/create-intent", async (req, res) => { + const { projectId, entrepreneurId } = req.body; + + try { + const paymentIntent = await stripe.paymentIntents.create({ + amount: 100000, // €1,000.00 in cents + currency: "eur", + metadata: { projectId, entrepreneurId }, + description: "Connect Africa — Project Submission Fee", + }); + + // Save to payments table + await supabase.from("payments").insert({ + project_id: projectId, + entrepreneur_id: entrepreneurId, + stripe_payment_intent_id: paymentIntent.id, + status: "pending", + }); + + res.json({ clientSecret: paymentIntent.client_secret }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); +``` + +### 11b. Stripe Webhook (backend) — update status on payment success + +```js +import { buffer } from "micro"; + +// POST /api/payments/webhook (set this URL in Stripe Dashboard) +app.post("/api/payments/webhook", + express.raw({ type: "application/json" }), + async (req, res) => { + const sig = req.headers["stripe-signature"]; + let event; + + try { + event = stripe.webhooks.constructEvent( + req.body, + sig, + process.env.STRIPE_WEBHOOK_SECRET + ); + } catch (err) { + return res.status(400).send(`Webhook Error: ${err.message}`); + } + + if (event.type === "payment_intent.succeeded") { + const pi = event.data.object; + + // Update payment status + await supabase + .from("payments") + .update({ status: "succeeded" }) + .eq("stripe_payment_intent_id", pi.id); + + // Update project status to under_review + await supabase + .from("projects") + .update({ + status: "under_review", + submitted_at: new Date().toISOString(), + }) + .eq("id", pi.metadata.projectId); + } + + res.json({ received: true }); + } +); +``` + +### 11c. Frontend — save verification slot and trigger payment + +In `Payment.jsx`, replace `handlePay`: + +```js +import { supabase } from "../../lib/supabase"; + +async function handlePay(e) { + e.preventDefault(); + const errs = validate(); + if (Object.keys(errs).length > 0) { + setErrors(errs); + window.scrollTo({ top: 0, behavior: "smooth" }); + return; + } + + setProcessing(true); + const { data: { user } } = await supabase.auth.getUser(); + + // 1. Create Stripe payment intent via backend + const res = await fetch(`${import.meta.env.VITE_API_URL}/api/payments/create-intent`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + projectId: project?.id, + entrepreneurId: user.id, + }), + }); + const { clientSecret, error } = await res.json(); + + if (error) { + setErrors({ cardNumber: error }); + setProcessing(false); + return; + } + + // 2. Confirm card payment with Stripe.js + // Install: npm install @stripe/stripe-js + const { loadStripe } = await import("@stripe/stripe-js"); + const stripeJs = await loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY); + + const { error: stripeError } = await stripeJs.confirmCardPayment(clientSecret, { + payment_method: { + card: { + // Use Stripe Elements in production instead of raw card fields + // See: https://stripe.com/docs/stripe-js/elements + number: cardNumber.replace(/\s/g, ""), + exp_month: parseInt(expiry.split("/")[0]), + exp_year: parseInt("20" + expiry.split("/")[1]), + cvc: cvv, + }, + billing_details: { name: cardName }, + }, + }); + + if (stripeError) { + setErrors({ cardNumber: stripeError.message }); + setProcessing(false); + return; + } + + // 3. Save verification date & time + await supabase + .from("payments") + .update({ + verification_date: selectedDate.toISOString().split("T")[0], + verification_time: selectedTime, + }) + .eq("project_id", project?.id) + .eq("entrepreneur_id", user.id); + + setProcessing(false); + setPaid(true); +} +``` + +> **Production note:** Use [Stripe Elements](https://stripe.com/docs/stripe-js/elements) instead of raw card fields. Raw card fields require PCI SAQ D compliance. Stripe Elements keeps you at SAQ A. + +--- + +## 12. Wire: Dashboard — Submissions & Chat (`Dashboard.jsx`) + +### 12a. Load submissions + +```js +import { useEffect } from "react"; +import { supabase } from "../../lib/supabase"; + +useEffect(() => { + async function loadData() { + const { data: { user } } = await supabase.auth.getUser(); + + const { data: submissions } = await supabase + .from("projects") + .select("*") + .eq("entrepreneur_id", user.id) + .order("created_at", { ascending: false }); + + if (submissions) setSubmissions(submissions); + } + loadData(); +}, []); +``` + +### 12b. Load messages + subscribe to realtime + +```js +useEffect(() => { + async function loadMessages() { + const { data: { user } } = await supabase.auth.getUser(); + + // Load history + const { data } = await supabase + .from("messages") + .select("*") + .eq("entrepreneur_id", user.id) + .order("created_at", { ascending: true }); + + if (data) setMessages(data); + + // Subscribe to new messages (realtime) + const channel = supabase + .channel("messages") + .on( + "postgres_changes", + { + event: "INSERT", + schema: "public", + table: "messages", + filter: `entrepreneur_id=eq.${user.id}`, + }, + (payload) => { + setMessages((prev) => [...prev, payload.new]); + } + ) + .subscribe(); + + return () => supabase.removeChannel(channel); + } + + loadMessages(); +}, []); +``` + +### 12c. Send a message + +```js +async function sendMessage() { + if (!newMsg.trim()) return; + const { data: { user } } = await supabase.auth.getUser(); + + await supabase.from("messages").insert({ + entrepreneur_id: user.id, + sender: "user", + sender_name: "Jane Doe", // replace with profile.first_name + " " + profile.last_name + text: newMsg.trim(), + }); + + setNewMsg(""); + // The realtime subscription above will add the message to state automatically +} +``` + +> **Enable Realtime for the `messages` table:** Supabase Dashboard → **Database → Replication** → toggle on `messages`. + +--- + +## 13. Wire: Project Feed (`ProjectFeed.jsx`) + +```js +import { useEffect } from "react"; +import { supabase } from "../../lib/supabase"; + +useEffect(() => { + async function loadProjects() { + const { data } = await supabase + .from("projects") + .select(` + id, title, category, stage, amount_seeking, country, + summary, views, interests, submitted_at, + entrepreneurs ( company_name, avatar_url ) + `) + .eq("status", "approved") + .order("submitted_at", { ascending: false }); + + if (data) setProjects(data); + } + loadProjects(); +}, []); +``` + +### Express interest + +```js +async function handleInterest(projectId) { + const { data: { user } } = await supabase.auth.getUser(); + + await supabase.from("project_interests").upsert( + { project_id: projectId, user_id: user.id }, + { onConflict: "project_id,user_id" } + ); + + // Increment counter via RPC (add this SQL function in Supabase) + await supabase.rpc("increment_project_interests", { pid: projectId }); +} +``` + +Add this SQL function in Supabase SQL Editor: + +```sql +create or replace function increment_project_interests(pid uuid) +returns void language sql as $$ + update public.projects + set interests = interests + 1 + where id = pid; +$$; +``` + +--- + +## 14. Express Backend Routes + +Summary of all API routes needed in `backend/server.js`: + +| Method | Path | Description | +|---|---|---| +| `POST` | `/api/payments/create-intent` | Create Stripe PaymentIntent, insert `payments` row | +| `POST` | `/api/payments/webhook` | Stripe webhook — update payment + project status | +| `GET` | `/api/projects/:id/download/:type` | Signed URL for pitch/business plan PDF download | + +### Signed PDF download (admin or investor use) + +```js +// GET /api/projects/:id/download/:type (type = "pitch" or "businessPlan") +app.get("/api/projects/:id/download/:type", async (req, res) => { + const { id, type } = req.params; + + // Fetch the project (use service role key here to bypass RLS) + const { data: project } = await supabase + .from("projects") + .select("pitch_url, business_plan_url") + .eq("id", id) + .single(); + + const path = type === "pitch" ? project.pitch_url : project.business_plan_url; + if (!path) return res.status(404).json({ error: "File not found" }); + + const { data } = await supabase.storage + .from("project-docs") + .createSignedUrl(path, 60 * 60); // 1 hour expiry + + res.json({ url: data.signedUrl }); +}); +``` + +--- + +## 15. Stripe Setup + +1. Create a Stripe account at [stripe.com](https://stripe.com) +2. Dashboard → **Developers → API Keys** — copy **Publishable key** and **Secret key** +3. Add to `.env`: + ``` + STRIPE_SECRET_KEY=sk_test_... + VITE_STRIPE_PUBLISHABLE_KEY=pk_test_... + ``` +4. Set up Webhook: + - Dashboard → **Developers → Webhooks → Add endpoint** + - URL: `https://your-backend.com/api/payments/webhook` + - Events to listen for: `payment_intent.succeeded`, `payment_intent.payment_failed` + - Copy the **Webhook signing secret** → add as `STRIPE_WEBHOOK_SECRET` +5. Install Stripe packages: + ```bash + cd backend && npm install stripe + cd frontend && npm install @stripe/stripe-js + ``` + +--- + +## 16. Deployment Checklist + +- [ ] All `.env` variables set in your hosting platform (Heroku / Render for backend; Vercel for frontend) +- [ ] Supabase **Redirect URL** set to your production frontend URL (`/welcome`) +- [ ] Supabase **Site URL** set to production URL in Auth settings +- [ ] Stripe webhook endpoint pointed to production backend URL +- [ ] `STRIPE_SECRET_KEY` switched from `sk_test_` to `sk_live_` +- [ ] Supabase Realtime enabled for `messages` table +- [ ] Storage bucket CORS configured if needed (Supabase Dashboard → Storage → Policies) +- [ ] `VITE_API_URL` in frontend `.env` points to production backend URL +- [ ] `frontend/src/lib/supabase.js` created (see Step 6) +- [ ] `vercel.json` already has SPA rewrite rule ✅ diff --git a/Build_Guide.md b/Build_Guide.md new file mode 100644 index 000000000..4627231d2 --- /dev/null +++ b/Build_Guide.md @@ -0,0 +1,1943 @@ +# Connect AFRICA — Web Application Build Guide + +## Complete Schema, Structure & Step-by-Step Cursor Instructions + +**Confidential — February 2026** +**Stack: Next.js (React) + Supabase + Stripe + WhatsApp Business API + OpenAI** + +--- + +## TABLE OF CONTENTS + +1. [Application Overview](#1-application-overview) +2. [Tech Stack & Dependencies](#2-tech-stack--dependencies) +3. [Project Folder Structure](#3-project-folder-structure) +4. [Supabase Database Schema](#4-supabase-database-schema) +5. [Authentication & Disclaimer Flow](#5-authentication--disclaimer-flow) +6. [Stripe Payment Architecture](#6-stripe-payment-architecture) +7. [Page-by-Page Breakdown](#7-page-by-page-breakdown) +8. [Step-by-Step Cursor Build Order](#8-step-by-step-cursor-build-order) +9. [API Routes & Edge Functions](#9-api-routes--edge-functions) +10. [Admin Panel](#10-admin-panel) +11. [AI Integration](#11-ai-integration) +12. [WhatsApp Business API Integration](#12-whatsapp-business-api-integration) +13. [Deployment & Environment Variables](#13-deployment--environment-variables) + +--- + +## 1. APPLICATION OVERVIEW + +Connect AFRICA is a LinkedIn-style investment platform connecting global investors with African entrepreneurs and verified service providers. The application has three distinct user types, each with unique flows, dashboards, and features. + +**Core Value Proposition:** Reduce deal friction and increase trust in cross-border African investments through structured listings, verification workflows, and controlled communication. + +**Three User Roles:** +- **Investor** — discovers ventures, filters by country/sector/stage, manages watchlists, requests introductions +- **Entrepreneur** — creates project listings (paid), uploads documents, receives investor interest +- **Service Provider** — subscribes for visibility, captures leads, earns verification badges + +--- + +## 2. TECH STACK & DEPENDENCIES + +### Core Framework +``` +next@14+ # React framework with App Router +react@18+ # UI library +typescript # Type safety +tailwindcss@3+ # Utility-first CSS +shadcn/ui # Component library (built on Radix) +``` + +### Backend & Database +``` +@supabase/supabase-js # Supabase client +@supabase/ssr # Supabase server-side rendering helpers +supabase CLI # Local dev, migrations, edge functions +``` + +### Payments +``` +stripe # Stripe Node.js SDK +@stripe/stripe-js # Stripe browser SDK +@stripe/react-stripe-js # Stripe React components +``` + +### AI & Messaging +``` +openai # OpenAI SDK for AI assistant features +whatsapp-business-api # WhatsApp Business API (or Meta Cloud API) +``` + +### Utilities +``` +zod # Schema validation +react-hook-form # Form management +@tanstack/react-query # Server state management +framer-motion # Animations (hero section, transitions) +lucide-react # Icon library +date-fns # Date utilities +react-player # Video player for hero section +sonner # Toast notifications +``` + +--- + +## 3. PROJECT FOLDER STRUCTURE + +``` +connect-africa/ +├── .env.local # Environment variables +├── supabase/ +│ ├── migrations/ # SQL migration files +│ │ ├── 001_create_profiles.sql +│ │ ├── 002_create_ventures.sql +│ │ ├── 003_create_providers.sql +│ │ ├── 004_create_investors.sql +│ │ ├── 005_create_payments.sql +│ │ ├── 006_create_messaging.sql +│ │ ├── 007_create_watchlists.sql +│ │ ├── 008_create_admin.sql +│ │ ├── 009_create_disclaimers.sql +│ │ └── 010_rls_policies.sql +│ ├── functions/ # Supabase Edge Functions +│ │ ├── stripe-webhook/ +│ │ ├── ai-assistant/ +│ │ ├── whatsapp-notify/ +│ │ └── admin-review/ +│ └── seed.sql # Seed data +├── public/ +│ ├── videos/ +│ │ └── hero-video.mp4 # Landing page hero video +│ ├── images/ +│ │ ├── logo.svg +│ │ ├── testimonials/ +│ │ └── icons/ +│ └── documents/ +│ ├── disclaimer-investor.pdf +│ ├── disclaimer-entrepreneur.pdf +│ └── disclaimer-provider.pdf +├── src/ +│ ├── app/ # Next.js App Router +│ │ ├── layout.tsx # Root layout +│ │ ├── page.tsx # Landing page +│ │ ├── globals.css +│ │ │ +│ │ ├── (auth)/ # Auth group +│ │ │ ├── login/page.tsx +│ │ │ ├── register/page.tsx # Role selection + signup +│ │ │ ├── register/disclaimer/page.tsx # Disclaimer signing +│ │ │ └── callback/route.ts # OAuth callback +│ │ │ +│ │ ├── (public)/ # Public pages +│ │ │ ├── about/page.tsx +│ │ │ ├── advice/ +│ │ │ │ ├── page.tsx # Advice hub +│ │ │ │ ├── investors/page.tsx +│ │ │ │ ├── entrepreneurs/page.tsx +│ │ │ │ └── providers/page.tsx +│ │ │ ├── rules/ +│ │ │ │ ├── page.tsx # Rules hub +│ │ │ │ ├── investors/page.tsx +│ │ │ │ ├── entrepreneurs/page.tsx +│ │ │ │ └── providers/page.tsx +│ │ │ └── terms/page.tsx +│ │ │ +│ │ ├── (dashboard)/ # Protected dashboard group +│ │ │ ├── layout.tsx # Dashboard layout with sidebar +│ │ │ ├── feed/page.tsx # Shared newsfeed +│ │ │ │ +│ │ │ ├── investor/ # Investor dashboard +│ │ │ │ ├── page.tsx # Investor home +│ │ │ │ ├── discover/page.tsx # Browse ventures +│ │ │ │ ├── watchlist/page.tsx +│ │ │ │ ├── deal-rooms/page.tsx +│ │ │ │ ├── deal-rooms/[id]/page.tsx +│ │ │ │ ├── messages/page.tsx +│ │ │ │ └── settings/page.tsx +│ │ │ │ +│ │ │ ├── entrepreneur/ # Entrepreneur dashboard +│ │ │ │ ├── page.tsx # Entrepreneur home +│ │ │ │ ├── project/new/page.tsx # Create listing +│ │ │ │ ├── project/[id]/page.tsx # Edit listing +│ │ │ │ ├── project/[id]/documents/page.tsx +│ │ │ │ ├── interest/page.tsx # Inbound interest +│ │ │ │ ├── providers/page.tsx # Browse providers +│ │ │ │ ├── messages/page.tsx +│ │ │ │ └── settings/page.tsx +│ │ │ │ +│ │ │ ├── provider/ # Service provider dashboard +│ │ │ │ ├── page.tsx # Provider home +│ │ │ │ ├── profile/page.tsx # Edit service profile +│ │ │ │ ├── leads/page.tsx # Incoming leads +│ │ │ │ ├── analytics/page.tsx +│ │ │ │ ├── subscription/page.tsx +│ │ │ │ ├── messages/page.tsx +│ │ │ │ └── settings/page.tsx +│ │ │ │ +│ │ │ └── admin/ # Admin panel +│ │ │ ├── page.tsx # Admin dashboard +│ │ │ ├── ventures/page.tsx # Review submissions +│ │ │ ├── providers/page.tsx # Provider verification +│ │ │ ├── users/page.tsx +│ │ │ ├── payments/page.tsx +│ │ │ ├── disputes/page.tsx +│ │ │ └── audit-log/page.tsx +│ │ │ +│ │ └── api/ # API routes +│ │ ├── stripe/ +│ │ │ ├── checkout/route.ts +│ │ │ ├── webhook/route.ts +│ │ │ ├── portal/route.ts +│ │ │ └── refund/route.ts +│ │ ├── ai/ +│ │ │ ├── profile-assist/route.ts +│ │ │ ├── memo-generate/route.ts +│ │ │ └── summarize/route.ts +│ │ └── whatsapp/ +│ │ ├── send/route.ts +│ │ └── webhook/route.ts +│ │ +│ ├── components/ +│ │ ├── ui/ # shadcn/ui components +│ │ ├── layout/ +│ │ │ ├── Navbar.tsx +│ │ │ ├── Footer.tsx +│ │ │ ├── DashboardSidebar.tsx +│ │ │ └── MobileNav.tsx +│ │ ├── landing/ +│ │ │ ├── HeroVideo.tsx # Full-page hero with video +│ │ │ ├── RoleButtons.tsx # 3 floating role buttons +│ │ │ ├── HowItWorks.tsx # Steps explanation +│ │ │ ├── Testimonials.tsx # Testimonials carousel +│ │ │ └── CTASection.tsx +│ │ ├── auth/ +│ │ │ ├── LoginForm.tsx +│ │ │ ├── RegisterForm.tsx +│ │ │ ├── RoleSelector.tsx +│ │ │ └── DisclaimerModal.tsx +│ │ ├── investor/ +│ │ │ ├── VentureCard.tsx +│ │ │ ├── FilterPanel.tsx +│ │ │ ├── WatchlistButton.tsx +│ │ │ ├── IntroRequestButton.tsx +│ │ │ └── DealRoomView.tsx +│ │ ├── entrepreneur/ +│ │ │ ├── ProjectForm.tsx +│ │ │ ├── DocumentChecklist.tsx +│ │ │ ├── MilestoneTracker.tsx +│ │ │ └── InterestCard.tsx +│ │ ├── provider/ +│ │ │ ├── ServiceProfileForm.tsx +│ │ │ ├── LeadCard.tsx +│ │ │ ├── SubscriptionManager.tsx +│ │ │ └── VerificationBadge.tsx +│ │ ├── messaging/ +│ │ │ ├── MessageThread.tsx +│ │ │ ├── MessageInput.tsx +│ │ │ └── IntroRequest.tsx +│ │ └── shared/ +│ │ ├── CountrySelector.tsx +│ │ ├── SectorSelector.tsx +│ │ ├── StageSelector.tsx +│ │ ├── FileUploader.tsx +│ │ ├── RiskBadge.tsx +│ │ └── ActivityFeed.tsx +│ │ +│ ├── lib/ +│ │ ├── supabase/ +│ │ │ ├── client.ts # Browser Supabase client +│ │ │ ├── server.ts # Server Supabase client +│ │ │ ├── admin.ts # Service role client +│ │ │ └── middleware.ts # Auth middleware +│ │ ├── stripe/ +│ │ │ ├── client.ts +│ │ │ ├── config.ts # Products & prices +│ │ │ └── helpers.ts +│ │ ├── ai/ +│ │ │ └── openai.ts +│ │ ├── whatsapp/ +│ │ │ └── client.ts +│ │ └── utils/ +│ │ ├── constants.ts # Countries, sectors, stages +│ │ ├── formatters.ts +│ │ └── validators.ts +│ │ +│ ├── hooks/ +│ │ ├── useUser.ts +│ │ ├── useRole.ts +│ │ ├── useVentures.ts +│ │ ├── useWatchlist.ts +│ │ ├── useMessages.ts +│ │ └── useSubscription.ts +│ │ +│ └── types/ +│ ├── database.ts # Generated Supabase types +│ ├── stripe.ts +│ └── index.ts +│ +├── middleware.ts # Next.js middleware (auth guard) +├── next.config.js +├── tailwind.config.ts +├── tsconfig.json +└── package.json +``` + +--- + +## 4. SUPABASE DATABASE SCHEMA + +### 4.1 Core Tables + +```sql +-- ============================================================ +-- MIGRATION 001: PROFILES (extends Supabase auth.users) +-- ============================================================ + +-- Enum types +CREATE TYPE user_role AS ENUM ('investor', 'entrepreneur', 'provider', 'admin'); +CREATE TYPE user_status AS ENUM ('pending_disclaimer', 'active', 'suspended', 'deactivated'); + +CREATE TABLE public.profiles ( + id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, + role user_role NOT NULL, + status user_status DEFAULT 'pending_disclaimer', + full_name TEXT NOT NULL, + email TEXT NOT NULL, + phone TEXT, + avatar_url TEXT, + country TEXT, -- ISO 3166-1 alpha-2 + city TEXT, + bio TEXT, + linkedin_url TEXT, + whatsapp_optin BOOLEAN DEFAULT FALSE, + disclaimer_signed BOOLEAN DEFAULT FALSE, + disclaimer_signed_at TIMESTAMPTZ, + disclaimer_version TEXT, -- tracks which version was signed + onboarding_completed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Auto-update timestamp +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER profiles_updated_at + BEFORE UPDATE ON profiles + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + + +-- ============================================================ +-- MIGRATION 002: VENTURES (Entrepreneur projects) +-- ============================================================ + +CREATE TYPE venture_status AS ENUM ( + 'draft', 'submitted', 'under_review', 'approved', + 'rejected', 'suspended', 'archived' +); +CREATE TYPE venture_stage AS ENUM ( + 'pre_seed', 'seed', 'series_a', 'series_b', + 'growth', 'bridge', 'other' +); + +CREATE TABLE public.ventures ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + entrepreneur_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + title TEXT NOT NULL, + slug TEXT UNIQUE, + tagline TEXT, + description TEXT, + country TEXT NOT NULL, -- primary country of operations + countries TEXT[], -- additional countries + sector TEXT NOT NULL, + sub_sector TEXT, + stage venture_stage NOT NULL, + funding_ask NUMERIC, -- amount seeking in EUR + currency TEXT DEFAULT 'EUR', + valuation NUMERIC, + revenue_ttm NUMERIC, -- trailing twelve months + team_size INTEGER, + founded_year INTEGER, + website TEXT, + pitch_deck_url TEXT, -- Supabase Storage path + logo_url TEXT, + milestones JSONB DEFAULT '[]', -- [{title, date, description}] + traction_data JSONB DEFAULT '{}', -- {users, revenue, growth_rate, etc.} + team_members JSONB DEFAULT '[]', -- [{name, role, linkedin, bio}] + risk_signals JSONB DEFAULT '{}', -- AI-generated or admin-set flags + status venture_status DEFAULT 'draft', + review_notes TEXT, -- admin notes (internal) + rejection_reason TEXT, + listing_fee_paid BOOLEAN DEFAULT FALSE, + listing_payment_id TEXT, -- Stripe payment intent ID + approved_at TIMESTAMPTZ, + approved_by UUID REFERENCES profiles(id), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_ventures_status ON ventures(status); +CREATE INDEX idx_ventures_country ON ventures(country); +CREATE INDEX idx_ventures_sector ON ventures(sector); +CREATE INDEX idx_ventures_stage ON ventures(stage); + +CREATE TRIGGER ventures_updated_at + BEFORE UPDATE ON ventures + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + + +-- ============================================================ +-- MIGRATION 003: VENTURE DOCUMENTS +-- ============================================================ + +CREATE TYPE document_type AS ENUM ( + 'pitch_deck', 'financials', 'cap_table', 'legal_docs', + 'business_plan', 'due_diligence', 'team_cv', 'other' +); + +CREATE TABLE public.venture_documents ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + venture_id UUID NOT NULL REFERENCES ventures(id) ON DELETE CASCADE, + uploaded_by UUID NOT NULL REFERENCES profiles(id), + doc_type document_type NOT NULL, + file_name TEXT NOT NULL, + file_path TEXT NOT NULL, -- Supabase Storage path + file_size BIGINT, + mime_type TEXT, + is_public BOOLEAN DEFAULT FALSE, -- visible to all investors or only in deal room + created_at TIMESTAMPTZ DEFAULT NOW() +); + + +-- ============================================================ +-- MIGRATION 004: INVESTOR PROFILES +-- ============================================================ + +CREATE TYPE investor_type AS ENUM ( + 'angel', 'vc', 'pe', 'family_office', 'impact', + 'diaspora', 'institutional', 'corporate', 'other' +); + +CREATE TABLE public.investor_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID UNIQUE NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + investor_type investor_type, + firm_name TEXT, + thesis TEXT, -- investment thesis + ticket_min NUMERIC, -- minimum ticket size EUR + ticket_max NUMERIC, + preferred_countries TEXT[], -- ISO codes + preferred_sectors TEXT[], + preferred_stages venture_stage[], + portfolio_size INTEGER, + tier TEXT DEFAULT 'free', -- 'free', 'pro', 'team' + stripe_customer_id TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TRIGGER investor_profiles_updated_at + BEFORE UPDATE ON investor_profiles + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + + +-- ============================================================ +-- MIGRATION 005: SERVICE PROVIDERS +-- ============================================================ + +CREATE TYPE provider_category AS ENUM ( + 'legal', 'accounting', 'tax', 'audit', 'consulting', + 'logistics', 'real_estate', 'hr', 'marketing', + 'technology', 'insurance', 'other' +); +CREATE TYPE provider_status AS ENUM ( + 'pending', 'active', 'suspended', 'expired' +); +CREATE TYPE verification_level AS ENUM ( + 'unverified', 'basic', 'verified', 'premium' +); + +CREATE TABLE public.service_providers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID UNIQUE NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + company_name TEXT NOT NULL, + category provider_category NOT NULL, + sub_categories TEXT[], + description TEXT, + services_offered JSONB DEFAULT '[]', -- [{name, description, price_range}] + countries_covered TEXT[] NOT NULL, + website TEXT, + logo_url TEXT, + verification_level verification_level DEFAULT 'unverified', + verification_notes TEXT, + verified_at TIMESTAMPTZ, + verified_by UUID REFERENCES profiles(id), + partner_references JSONB DEFAULT '[]', -- [{name, contact, relationship}] + pricing_info TEXT, + availability TEXT, + status provider_status DEFAULT 'pending', + subscription_tier TEXT, -- 'monthly', 'annual' + stripe_customer_id TEXT, + stripe_subscription_id TEXT, + subscription_expires_at TIMESTAMPTZ, + lead_count INTEGER DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_providers_category ON service_providers(category); +CREATE INDEX idx_providers_countries ON service_providers USING GIN(countries_covered); +CREATE INDEX idx_providers_status ON service_providers(status); + +CREATE TRIGGER service_providers_updated_at + BEFORE UPDATE ON service_providers + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + + +-- ============================================================ +-- MIGRATION 006: PAYMENTS & TRANSACTIONS +-- ============================================================ + +CREATE TYPE payment_type AS ENUM ( + 'listing_fee', 'provider_subscription', 'investor_upgrade', + 'refund' +); +CREATE TYPE payment_status AS ENUM ( + 'pending', 'completed', 'failed', 'refunded', 'partially_refunded' +); + +CREATE TABLE public.payments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES profiles(id), + payment_type payment_type NOT NULL, + amount NUMERIC NOT NULL, + currency TEXT DEFAULT 'EUR', + stripe_payment_intent_id TEXT, + stripe_checkout_session_id TEXT, + stripe_subscription_id TEXT, + status payment_status DEFAULT 'pending', + refund_amount NUMERIC, + refund_reason TEXT, + metadata JSONB DEFAULT '{}', -- flexible extra data + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_payments_user ON payments(user_id); +CREATE INDEX idx_payments_stripe ON payments(stripe_payment_intent_id); + +CREATE TRIGGER payments_updated_at + BEFORE UPDATE ON payments + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + + +-- ============================================================ +-- MIGRATION 007: MESSAGING & INTRO REQUESTS +-- ============================================================ + +CREATE TYPE intro_status AS ENUM ( + 'pending', 'accepted', 'declined', 'expired' +); + +CREATE TABLE public.intro_requests ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + from_user_id UUID NOT NULL REFERENCES profiles(id), + to_user_id UUID NOT NULL REFERENCES profiles(id), + venture_id UUID REFERENCES ventures(id), + message TEXT, + status intro_status DEFAULT 'pending', + responded_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_intro_from ON intro_requests(from_user_id); +CREATE INDEX idx_intro_to ON intro_requests(to_user_id); + +CREATE TABLE public.conversations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + participant_ids UUID[] NOT NULL, -- array of user IDs + venture_id UUID REFERENCES ventures(id), + intro_request_id UUID REFERENCES intro_requests(id), + is_deal_room BOOLEAN DEFAULT FALSE, + title TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE public.messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, + sender_id UUID NOT NULL REFERENCES profiles(id), + content TEXT NOT NULL, + attachments JSONB DEFAULT '[]', -- [{file_name, file_path, mime_type}] + read_by UUID[] DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_messages_conversation ON messages(conversation_id); +CREATE INDEX idx_messages_created ON messages(created_at DESC); + + +-- ============================================================ +-- MIGRATION 008: WATCHLISTS & SAVED ITEMS +-- ============================================================ + +CREATE TABLE public.watchlist_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + venture_id UUID NOT NULL REFERENCES ventures(id) ON DELETE CASCADE, + notes TEXT, + alert_on_update BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(user_id, venture_id) +); + +CREATE TABLE public.saved_providers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + provider_id UUID NOT NULL REFERENCES service_providers(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(user_id, provider_id) +); + + +-- ============================================================ +-- MIGRATION 009: DEAL ROOMS +-- ============================================================ + +CREATE TABLE public.deal_rooms ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + venture_id UUID NOT NULL REFERENCES ventures(id), + created_by UUID NOT NULL REFERENCES profiles(id), + title TEXT NOT NULL, + member_ids UUID[] NOT NULL, + status TEXT DEFAULT 'active', -- 'active', 'closed', 'archived' + notes JSONB DEFAULT '[]', -- [{user_id, content, timestamp}] + shared_documents UUID[], -- references to venture_documents + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + + +-- ============================================================ +-- MIGRATION 010: DISCLAIMERS & LEGAL +-- ============================================================ + +CREATE TABLE public.disclaimers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + version TEXT NOT NULL UNIQUE, -- e.g., 'v1.0', 'v1.1' + role user_role NOT NULL, -- different disclaimer per role + title TEXT NOT NULL, + content TEXT NOT NULL, -- full legal text (markdown) + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE public.disclaimer_signatures ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + disclaimer_id UUID NOT NULL REFERENCES disclaimers(id), + ip_address INET, + user_agent TEXT, + signature_data JSONB, -- {full_name, agreed: true, timestamp} + signed_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_signatures_user ON disclaimer_signatures(user_id); + + +-- ============================================================ +-- MIGRATION 011: ADMIN & AUDIT +-- ============================================================ + +CREATE TYPE audit_action AS ENUM ( + 'venture_submitted', 'venture_approved', 'venture_rejected', + 'venture_suspended', 'provider_verified', 'provider_suspended', + 'user_suspended', 'user_activated', 'refund_issued', + 'payment_received', 'intro_sent', 'intro_accepted', + 'intro_declined', 'document_uploaded', 'profile_updated', + 'disclaimer_signed', 'deal_room_created', 'message_sent', + 'admin_action' +); + +CREATE TABLE public.audit_log ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + actor_id UUID REFERENCES profiles(id), + action audit_action NOT NULL, + target_type TEXT, -- 'venture', 'user', 'payment', etc. + target_id UUID, + details JSONB DEFAULT '{}', -- flexible context + ip_address INET, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_audit_actor ON audit_log(actor_id); +CREATE INDEX idx_audit_action ON audit_log(action); +CREATE INDEX idx_audit_target ON audit_log(target_type, target_id); +CREATE INDEX idx_audit_created ON audit_log(created_at DESC); + + +-- ============================================================ +-- MIGRATION 012: NEWSFEED / ACTIVITY +-- ============================================================ + +CREATE TABLE public.feed_posts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + author_id UUID NOT NULL REFERENCES profiles(id), + content TEXT NOT NULL, + post_type TEXT DEFAULT 'update', -- 'update', 'milestone', 'announcement', 'article' + venture_id UUID REFERENCES ventures(id), + attachments JSONB DEFAULT '[]', + visibility TEXT DEFAULT 'public', -- 'public', 'investors_only', 'private' + likes_count INTEGER DEFAULT 0, + comments_count INTEGER DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE public.feed_comments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + post_id UUID NOT NULL REFERENCES feed_posts(id) ON DELETE CASCADE, + author_id UUID NOT NULL REFERENCES profiles(id), + content TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE public.feed_likes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + post_id UUID NOT NULL REFERENCES feed_posts(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(post_id, user_id) +); + + +-- ============================================================ +-- MIGRATION 013: PROVIDER LEADS +-- ============================================================ + +CREATE TABLE public.provider_leads ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + provider_id UUID NOT NULL REFERENCES service_providers(id) ON DELETE CASCADE, + from_user_id UUID NOT NULL REFERENCES profiles(id), + venture_id UUID REFERENCES ventures(id), + message TEXT, + status TEXT DEFAULT 'new', -- 'new', 'contacted', 'converted', 'closed' + created_at TIMESTAMPTZ DEFAULT NOW() +); + + +-- ============================================================ +-- MIGRATION 014: ROW LEVEL SECURITY (RLS) +-- ============================================================ + +ALTER TABLE profiles ENABLE ROW LEVEL SECURITY; +ALTER TABLE ventures ENABLE ROW LEVEL SECURITY; +ALTER TABLE venture_documents ENABLE ROW LEVEL SECURITY; +ALTER TABLE investor_profiles ENABLE ROW LEVEL SECURITY; +ALTER TABLE service_providers ENABLE ROW LEVEL SECURITY; +ALTER TABLE payments ENABLE ROW LEVEL SECURITY; +ALTER TABLE intro_requests ENABLE ROW LEVEL SECURITY; +ALTER TABLE conversations ENABLE ROW LEVEL SECURITY; +ALTER TABLE messages ENABLE ROW LEVEL SECURITY; +ALTER TABLE watchlist_items ENABLE ROW LEVEL SECURITY; +ALTER TABLE saved_providers ENABLE ROW LEVEL SECURITY; +ALTER TABLE deal_rooms ENABLE ROW LEVEL SECURITY; +ALTER TABLE disclaimers ENABLE ROW LEVEL SECURITY; +ALTER TABLE disclaimer_signatures ENABLE ROW LEVEL SECURITY; +ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY; +ALTER TABLE feed_posts ENABLE ROW LEVEL SECURITY; +ALTER TABLE feed_comments ENABLE ROW LEVEL SECURITY; +ALTER TABLE feed_likes ENABLE ROW LEVEL SECURITY; +ALTER TABLE provider_leads ENABLE ROW LEVEL SECURITY; + +-- PROFILES: users can read all active profiles, edit own +CREATE POLICY "Public profiles readable" + ON profiles FOR SELECT + USING (status = 'active' OR id = auth.uid()); + +CREATE POLICY "Users update own profile" + ON profiles FOR UPDATE + USING (id = auth.uid()); + +-- VENTURES: approved ventures public, own ventures always visible +CREATE POLICY "Approved ventures public" + ON ventures FOR SELECT + USING (status = 'approved' OR entrepreneur_id = auth.uid()); + +CREATE POLICY "Entrepreneurs manage own ventures" + ON ventures FOR ALL + USING (entrepreneur_id = auth.uid()); + +-- ADMIN: full access for admin role +CREATE POLICY "Admin full access profiles" + ON profiles FOR ALL + USING ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') + ); + +-- (Additional RLS policies follow same pattern for all tables) +-- Investors can see approved ventures +-- Providers can manage own profiles +-- Messages visible only to conversation participants +-- Audit log admin-only +-- Watchlists are private to the owner +``` + +### 4.2 Supabase Storage Buckets + +``` +Buckets to create: +├── avatars/ # Profile photos (public) +├── venture-logos/ # Venture logos (public) +├── venture-docs/ # Pitch decks, financials (private, RLS) +├── provider-logos/ # Provider logos (public) +├── feed-attachments/ # Newsfeed images/files (public) +└── legal-docs/ # Signed disclaimers (private, admin-only) +``` + +--- + +## 5. AUTHENTICATION & DISCLAIMER FLOW + +### Registration Flow (Critical — All Users Must Sign Disclaimer) + +``` +1. User lands on /register +2. User selects role: Investor | Entrepreneur | Service Provider +3. User enters: email, password, full name, country +4. Supabase Auth creates account → profile row created via trigger +5. Profile status set to 'pending_disclaimer' +6. Redirect to /register/disclaimer +7. Show role-specific disclaimer (full legal text, scrollable) +8. User must: + a. Scroll to bottom (or checkbox "I have read the above") + b. Type full name as electronic signature + c. Check "I agree to the terms and conditions" + d. Click "Sign & Continue" +9. Record in disclaimer_signatures table (with IP, user_agent, timestamp) +10. Update profile: disclaimer_signed = true, status = 'active' +11. Redirect to role-specific onboarding or dashboard +``` + +### Middleware Guard Logic + +```typescript +// middleware.ts — enforces disclaimer signing +// If user is logged in but disclaimer_signed = false, +// redirect to /register/disclaimer for ANY protected route +// If user is not logged in, redirect to /login for protected routes +// Admin routes require role = 'admin' +``` + +--- + +## 6. STRIPE PAYMENT ARCHITECTURE + +### 6.1 Products & Prices + +``` +Stripe Products: +├── Entrepreneur Listing Fee +│ └── Price: €1,000 one-time (payment_type: 'listing_fee') +│ └── Metadata: { refundable_portion: 800, non_refundable: 200 } +│ +├── Service Provider Subscription +│ ├── Monthly: €X/month (payment_type: 'provider_subscription') +│ └── Annual: €Y/year (payment_type: 'provider_subscription') +│ +└── Investor Premium + ├── Pro Tier: €X/month (advanced filters, deal rooms) + └── Team Tier: €X/month (multi-seat, internal approvals) +``` + +### 6.2 Payment Flows + +**Entrepreneur Listing Payment:** +``` +1. Entrepreneur completes project draft +2. Clicks "Submit for Review" → Stripe Checkout (€1,000) +3. Webhook: payment_intent.succeeded + → Create payment record + → Update venture: listing_fee_paid = true, status = 'submitted' + → Log audit event +4. Admin reviews → approve/reject +5. If rejected: + → Admin triggers partial refund (€800 refundable portion) + → Platform keeps €200 non-refundable review fee +``` + +**Provider Subscription:** +``` +1. Provider completes profile +2. Selects plan → Stripe Checkout (subscription) +3. Webhook: customer.subscription.created + → Update provider: subscription_tier, subscription_expires_at + → Activate provider listing +4. Webhook: customer.subscription.deleted + → Deactivate provider listing +``` + +**Investor Upgrade:** +``` +1. Investor on free tier → clicks upgrade +2. Stripe Checkout (subscription) +3. Webhook updates investor tier +``` + +--- + +## 7. PAGE-BY-PAGE BREAKDOWN + +### 7.1 Landing Page (`/` — `src/app/page.tsx`) + +``` +SECTION 1: HERO (full viewport height) +┌─────────────────────────────────────────────────┐ +│ │ +│ [Full-screen background video playing] │ +│ (autoplay, muted, loop) │ +│ │ +│ Overlay: semi-transparent dark gradient │ +│ │ +│ Center: │ +│ CONNECT AFRICA logo (large, white) │ +│ "The trust layer for cross-border │ +│ investment into Africa" │ +│ │ +│ Bottom floating buttons (3 columns): │ +│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ +│ │ INVESTOR │ │ENTREPRENEUR│ │ SERVICE │ │ +│ │ "Find │ │ "Get your │ │ PROVIDER │ │ +│ │ vetted │ │ venture │ │ "Connect │ │ +│ │ deals" │ │ funded" │ │ with │ │ +│ │ │ │ │ │ clients" │ │ +│ └────────────┘ └────────────┘ └────────────┘ │ +│ (Each button links to /register?role=X) │ +└─────────────────────────────────────────────────┘ + +SECTION 2: HOW IT WORKS (below the fold) +┌─────────────────────────────────────────────────┐ +│ "How Connect AFRICA Works" │ +│ │ +│ Three columns (responsive → stacked on mobile): │ +│ │ +│ FOR INVESTORS: FOR ENTREPRENEURS: │ +│ 1. Create profile 1. Pay listing fee │ +│ 2. Browse ventures 2. Build your profile │ +│ 3. Request intros 3. Get reviewed/approved │ +│ 4. Enter deal rooms 4. Receive investor │ +│ interest │ +│ │ +│ FOR SERVICE PROVIDERS: │ +│ 1. Subscribe │ +│ 2. Get verified │ +│ 3. Receive leads │ +│ 4. Close deals │ +│ │ +│ [CTA: "Join the Platform →"] │ +└─────────────────────────────────────────────────┘ + +SECTION 3: WHY CONNECT AFRICA +┌─────────────────────────────────────────────────┐ +│ Problem → Solution grid │ +│ "Information asymmetry" → "Standardized data" │ +│ "Trust deficits" → "Paid listings + verification"│ +│ "High deal friction" → "Structured workflows" │ +└─────────────────────────────────────────────────┘ + +SECTION 4: PLATFORM STATS / SOCIAL PROOF +┌─────────────────────────────────────────────────┐ +│ Animated counters: │ +│ "54 African Nations" | "X Ventures" | "X │ +│ Investors" | "X Service Providers" │ +└─────────────────────────────────────────────────┘ + +SECTION 5: TESTIMONIALS +┌─────────────────────────────────────────────────┐ +│ "What Our Users Say" │ +│ │ +│ Carousel of testimonial cards: │ +│ ┌──────────────────────┐ │ +│ │ Photo | Name | Role │ │ +│ │ "Quote text..." │ │ +│ │ Company / Country │ │ +│ └──────────────────────┘ │ +│ │ +│ Categories: Investor | Entrepreneur | Provider │ +│ (filter tabs above carousel) │ +└─────────────────────────────────────────────────┘ + +SECTION 6: CTA + FOOTER +┌─────────────────────────────────────────────────┐ +│ "Ready to connect?" │ +│ [Get Started] [Learn More] │ +│ │ +│ Footer: Links to Advice, Rules, Terms, About │ +└─────────────────────────────────────────────────┘ +``` + +### 7.2 Advice Pages (`/advice/*`) + +Each role gets a dedicated advice page with practical guidance: + +**`/advice/investors`** — Advice for Investors +- Understanding African markets: cultural context, relationship-first reality +- Due diligence checklist for cross-border deals +- Risk assessment framework (what "good" looks like per country/sector) +- Common failure patterns and how to avoid them +- Working with local service providers +- Timeline expectations vs. Western norms +- Post-investment governance best practices + +**`/advice/entrepreneurs`** — Advice for Entrepreneurs +- What investors actually look for (structured data, clear metrics) +- How to build an investor-ready profile +- Document preparation checklist by stage +- Pricing your round and setting realistic valuations +- Communicating traction effectively +- Working with service providers for compliance +- Managing investor relationships post-intro + +**`/advice/providers`** — Advice for Service Providers +- How to build a compelling provider profile +- Verification process and how to earn premium badges +- Lead conversion best practices +- Pricing transparency expectations +- Country coverage and specialization strategy +- Building reputation through platform references + +### 7.3 Rules Pages (`/rules/*`) + +**`/rules` — Platform Rules Hub** (links to all three below) + +**`/rules/investors`** — Investor Rules +- Account verification requirements +- Communication guidelines (no spam, no unsolicited bulk messages) +- Intro request limits and etiquette +- Confidentiality obligations for deal room documents +- Prohibited activities (front-running, sharing deal info externally) +- Dispute resolution process +- Account suspension/termination conditions + +**`/rules/entrepreneurs`** — Entrepreneur Rules +- Listing accuracy and truthfulness requirements +- Mandatory disclosures (conflicts, legal issues, material risks) +- Document authenticity standards +- Response time expectations for investor inquiries +- Prohibited listings (illegal activities, sanctions violations) +- Refund policy for rejected listings +- Profile maintenance and update obligations + +**`/rules/providers`** — Service Provider Rules +- Verification truthfulness requirements +- Service delivery standards +- Pricing transparency obligations +- Client confidentiality requirements +- Conflict of interest disclosure +- Subscription terms and cancellation +- Lead response time expectations +- Grounds for badge revocation + +### 7.4 Dashboard Pages (Protected) + +**Shared Newsfeed** — `/feed` +- Posts from ventures (milestones, updates) +- Platform announcements +- Provider highlights +- Like/comment functionality + +**Investor Dashboard** — `/investor` +- Overview: recent activity, watchlist updates, pending intros +- Discover: filterable venture grid (country, sector, stage, risk) +- Watchlist: saved ventures with alert preferences +- Deal Rooms: active rooms with document tracking +- Messages: conversations from accepted intros + +**Entrepreneur Dashboard** — `/entrepreneur` +- Overview: listing status, inbound interest count, profile completeness +- Project management: create/edit project, upload documents +- Interest: inbound intro requests (accept/decline) +- Providers: browse vetted service providers +- Messages: conversations with investors + +**Provider Dashboard** — `/provider` +- Overview: lead count, subscription status, analytics +- Profile: edit services, coverage, pricing +- Leads: incoming lead management +- Analytics: views, clicks, conversion rates +- Subscription: manage plan, billing + +--- + +## 8. STEP-BY-STEP CURSOR BUILD ORDER + +### PHASE 1: Project Scaffolding (Steps 1–5) + +**Step 1: Initialize Next.js Project** +``` +Cursor prompt: "Create a new Next.js 14 project with TypeScript, Tailwind CSS, +App Router, and src/ directory. Install shadcn/ui with the default config. +Add Supabase client packages (@supabase/supabase-js, @supabase/ssr). +Add Stripe packages (stripe, @stripe/stripe-js, @stripe/react-stripe-js). +Add react-player, framer-motion, zod, react-hook-form, @tanstack/react-query, +lucide-react, date-fns, sonner." +``` + +**Step 2: Set Up Environment Variables** +``` +Cursor prompt: "Create .env.local with placeholders for: +NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, +SUPABASE_SERVICE_ROLE_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, +STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, OPENAI_API_KEY, +WHATSAPP_API_TOKEN, NEXT_PUBLIC_APP_URL. +Create a lib/env.ts that validates these with zod." +``` + +**Step 3: Initialize Supabase** +``` +Cursor prompt: "Set up Supabase clients in src/lib/supabase/: +- client.ts: browser client using createBrowserClient +- server.ts: server client using createServerClient with cookies +- admin.ts: service role client for admin operations +- middleware.ts: auth helper for Next.js middleware +Create the Next.js middleware.ts at root that checks auth status and +redirects unauthenticated users from protected routes." +``` + +**Step 4: Run Database Migrations** +``` +Cursor prompt: "Create all Supabase SQL migration files in supabase/migrations/ +using the complete database schema provided. Include all tables, types, indexes, +triggers, and RLS policies. Create a seed.sql with: +- 3 disclaimer records (one per role) +- An admin user profile +- Sample sector/country reference data." +``` + +**Step 5: Generate TypeScript Types** +``` +Cursor prompt: "Run supabase gen types typescript to generate database types. +Create src/types/database.ts with the generated types. Create src/types/index.ts +with application-level types extending the database types." +``` + +### PHASE 2: Layout & Shared Components (Steps 6–9) + +**Step 6: Create Root Layout & Global Styles** +``` +Cursor prompt: "Create the root layout.tsx with: +- Supabase auth provider +- React Query provider +- Sonner toaster +- Global metadata (title, description, OG tags for Connect AFRICA) +Set up globals.css with Tailwind config, custom CSS variables for brand colors +(Africa-inspired: gold #D4A843, deep green #1B5E20, warm brown #5D4037, +sky blue #0277BD). Configure tailwind.config.ts with these custom colors." +``` + +**Step 7: Build Navbar & Footer** +``` +Cursor prompt: "Create components/layout/Navbar.tsx: +- Logo left, nav links center, auth buttons right +- Mobile hamburger menu +- Conditional rendering based on auth state (Login/Register vs Dashboard link) +- Role-aware dashboard link + +Create components/layout/Footer.tsx: +- Site links: About, Advice, Rules, Terms +- Social links +- Copyright +- Contact info placeholder" +``` + +**Step 8: Build Dashboard Layout** +``` +Cursor prompt: "Create the (dashboard)/layout.tsx with: +- Left sidebar navigation (collapsible on mobile) +- Role-aware menu items (different nav for investor/entrepreneur/provider/admin) +- Top bar with user avatar, notifications bell, role badge +- Main content area +Use shadcn/ui Sheet for mobile sidebar." +``` + +**Step 9: Build Shared Components** +``` +Cursor prompt: "Create these shared components: +- CountrySelector: dropdown of 54 African nations with flags +- SectorSelector: multi-select for investment sectors +- StageSelector: venture stage picker +- FileUploader: drag-and-drop with Supabase Storage integration +- RiskBadge: colored badge showing risk level +- ActivityFeed: reusable activity stream component +Use shadcn/ui Select, MultiSelect, and Badge components." +``` + +### PHASE 3: Landing Page (Steps 10–13) + +**Step 10: Build Hero Video Section** +``` +Cursor prompt: "Create components/landing/HeroVideo.tsx: +- Full viewport height (100vh) container +- Background video using react-player or HTML5