โœ“ Copied!
CONDUCTOR AI ยท SETUP GUIDE
๐Ÿ“ฑ Main App ๐ŸŒ Live App โš™๏ธ Admin
๐Ÿ—๏ธ System Architecture
Conductor AI runs entirely on frontend + Supabase. No dedicated backend server needed. Here's how all the pieces connect.
Browser
HTML App โ†’ Supabase JS SDK โ†’ Auth + DB + Realtime
Database
PostgreSQL fare_reports ยท feed_posts ยท traffic_updates ยท alerts
Realtime
Supabase Realtime โ†’ WebSocket pushes DB changes to all browsers instantly
AI Agents
Edge Functions โ†’ OpenRouter API โ†’ Llama 3.1, Gemma 2, Mistral (FREE)
Scheduler
pg_cron โ†’ Triggers Edge Functions every N minutes automatically โ€” no server needed
Hosting
Netlify / Vercel or GitHub Pages or WhatsApp Link โ€” all FREE
๐Ÿ’ก
Why no backend? Supabase Edge Functions run on Deno servers โ€” they handle all server-side logic (API keys, AI calls, data processing) without you managing any server. pg_cron replaces cron jobs. Realtime replaces WebSocket servers.
๐Ÿ“‹ Prerequisites
Everything you need before starting. All free.
ToolPurposeFree TierLink
SupabaseDatabase, Auth, Realtime, Edge Functionsโœ… 500MB DB, 2M edge invocationssupabase.com
OpenRouterAccess to 100+ AI models via one APIโœ… Free models (Llama, Gemma, Mistral)openrouter.ai
Supabase CLIDeploy Edge Functions from terminalโœ… Freesupabase.com/docs/guides/cli
Netlify/VercelHost the HTML appโœ… Generous free tiernetlify.com / vercel.com
Node.jsRequired for Supabase CLIโœ… Freenodejs.org
โš ๏ธ Get your OpenRouter API key at openrouter.ai/keys before starting Edge Functions setup. You'll need it as a secret.
โ‘  Create Supabase Project
Create your free project and get the credentials needed for the app.
1
Create Project at supabase.com
FREE

Go to supabase.com โ†’ New Project โ†’ choose a region close to Nigeria (Europe West or US East are fastest) โ†’ set a strong database password.

๐Ÿ’ก Save your database password somewhere safe โ€” you'll need it for CLI login.
2
Get Your API Credentials

Go to Settings โ†’ API in your Supabase dashboard. You need:

CredentialWhere to FindUsed For
Project URLSettings โ†’ API โ†’ Project URLSupabase SDK init
anon publicSettings โ†’ API โ†’ anon public keyFrontend SDK (safe to expose)
service_roleSettings โ†’ API โ†’ service_role keyEdge Functions only โ€” NEVER expose in frontend
โ‘ก Database Schema
Run this SQL in your Supabase SQL Editor (Database โ†’ SQL Editor โ†’ New Query). Creates all tables needed for the app.
๐Ÿ’ก Go to your Supabase Dashboard โ†’ SQL Editor โ†’ paste each block โ†’ Run
COMPLETE SCHEMA SQL
-- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-- CONDUCTOR AI ยท SUPABASE SCHEMA
-- Run this entire block in SQL Editor
-- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_cron";

-- โ”€โ”€โ”€ FARE REPORTS โ”€โ”€โ”€
-- Stores crowdsourced fare data from users
CREATE TABLE fare_reports (
  id            uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
  city          text NOT NULL,
  from_area     text NOT NULL,
  to_area       text NOT NULL,
  transport_mode text NOT NULL,
  fare_amount   integer NOT NULL,
  time_slot     text,
  duration_label text,
  duration_mins integer,
  travel_date   date,
  note          text,
  user_id       uuid REFERENCES auth.users(id),
  username      text,
  verified      boolean DEFAULT false,
  created_at    timestamptz DEFAULT now()
);

-- โ”€โ”€โ”€ TRAFFIC UPDATES โ”€โ”€โ”€
-- AI-generated and crowdsourced traffic data
CREATE TABLE traffic_updates (
  id          uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
  city        text NOT NULL,
  level       text NOT NULL CHECK (level IN ('low', 'medium', 'high', 'severe')),
  alert       text,
  routes_affected text[],
  source      text DEFAULT 'ai',
  expires_at  timestamptz DEFAULT now() + INTERVAL '2 hours',
  created_at  timestamptz DEFAULT now()
);

-- โ”€โ”€โ”€ CITY ALERTS โ”€โ”€โ”€
-- Persistent alerts shown on homepage
CREATE TABLE city_alerts (
  id          uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
  city        text NOT NULL,
  type        text,
  severity    text DEFAULT 'info',
  title       text,
  body        text,
  active      boolean DEFAULT true,
  expires_at  timestamptz,
  created_at  timestamptz DEFAULT now()
);

-- โ”€โ”€โ”€ FEED POSTS โ”€โ”€โ”€
-- Community road updates
CREATE TABLE feed_posts (
  id          bigserial PRIMARY KEY,
  content     text NOT NULL,
  city        text NOT NULL,
  type        text DEFAULT 'general',
  username    text,
  user_id     uuid REFERENCES auth.users(id),
  likes       integer DEFAULT 0,
  moderated   boolean DEFAULT false,
  approved    boolean DEFAULT true,
  created_at  timestamptz DEFAULT now()
);

-- โ”€โ”€โ”€ POST LIKES โ”€โ”€โ”€
CREATE TABLE post_likes (
  user_id   uuid REFERENCES auth.users(id),
  post_id   bigint REFERENCES feed_posts(id) ON DELETE CASCADE,
  created_at timestamptz DEFAULT now(),
  PRIMARY KEY (user_id, post_id)
);

-- โ”€โ”€โ”€ FARE INDEX โ”€โ”€โ”€
-- Aggregated fare data per route (updated by AI agent)
CREATE TABLE fare_index (
  id              uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
  city            text NOT NULL,
  from_area       text NOT NULL,
  to_area         text NOT NULL,
  transport_mode  text NOT NULL,
  avg_fare        integer,
  min_fare        integer,
  max_fare        integer,
  sample_count    integer DEFAULT 0,
  trend           text DEFAULT 'same',
  last_updated    timestamptz DEFAULT now(),
  UNIQUE(city, from_area, to_area, transport_mode)
);

-- โ”€โ”€โ”€ USER PROFILES โ”€โ”€โ”€
CREATE TABLE user_profiles (
  id           uuid REFERENCES auth.users(id) PRIMARY KEY,
  username     text,
  home_city    text DEFAULT 'Lagos',
  points       integer DEFAULT 0,
  reports_count integer DEFAULT 0,
  streak_days  integer DEFAULT 0,
  last_active  date DEFAULT CURRENT_DATE,
  created_at   timestamptz DEFAULT now()
);

-- โ”€โ”€โ”€ CHAT LOGS โ”€โ”€โ”€
CREATE TABLE chat_logs (
  id         bigserial PRIMARY KEY,
  user_id    uuid REFERENCES auth.users(id),
  message    text,
  response   text,
  created_at timestamptz DEFAULT now()
);

-- โ”€โ”€โ”€ AGENT RUNS LOG โ”€โ”€โ”€
CREATE TABLE agent_runs (
  id          bigserial PRIMARY KEY,
  agent_name  text,
  model       text,
  tokens_used integer,
  status      text,
  output      jsonb,
  created_at  timestamptz DEFAULT now()
);

-- โ”€โ”€โ”€ USEFUL VIEWS โ”€โ”€โ”€
CREATE VIEW fare_summary AS
SELECT
  city,
  from_area,
  to_area,
  transport_mode,
  ROUND(AVG(fare_amount)) AS avg_fare,
  MIN(fare_amount) AS min_fare,
  MAX(fare_amount) AS max_fare,
  COUNT(*) AS sample_count
FROM fare_reports
WHERE created_at > now() - INTERVAL '30 days'
GROUP BY city, from_area, to_area, transport_mode
ORDER BY sample_count DESC;

-- Auto-create profile on signup
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.user_profiles (id, username)
  VALUES (NEW.id, SPLIT_PART(NEW.email, '@', 1));
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE handle_new_user();

-- Award points on fare report
CREATE OR REPLACE FUNCTION award_report_points()
RETURNS TRIGGER AS $$
BEGIN
  IF NEW.user_id IS NOT NULL THEN
    UPDATE user_profiles
    SET
      points = points + 50,
      reports_count = reports_count + 1
    WHERE id = NEW.user_id;
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_fare_report_created
  AFTER INSERT ON fare_reports
  FOR EACH ROW EXECUTE award_report_points();

-- Indexes for performance
CREATE INDEX idx_fare_reports_city ON fare_reports(city);
CREATE INDEX idx_fare_reports_route ON fare_reports(from_area, to_area);
CREATE INDEX idx_traffic_city ON traffic_updates(city, created_at DESC);
CREATE INDEX idx_feed_city ON feed_posts(city, created_at DESC);
โ‘ข Row Level Security (RLS)
RLS ensures users can only read/write their own data. Run this after the schema.
RLS POLICIES SQL
-- Enable RLS on all tables
ALTER TABLE fare_reports ENABLE ROW LEVEL SECURITY;
ALTER TABLE traffic_updates ENABLE ROW LEVEL SECURITY;
ALTER TABLE city_alerts ENABLE ROW LEVEL SECURITY;
ALTER TABLE feed_posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE post_likes ENABLE ROW LEVEL SECURITY;
ALTER TABLE fare_index ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE chat_logs ENABLE ROW LEVEL SECURITY;

-- fare_reports: anyone can read, authenticated users can insert
CREATE POLICY "Public read fare_reports" ON fare_reports
  FOR SELECT USING (true);
CREATE POLICY "Auth users insert fare_reports" ON fare_reports
  FOR INSERT WITH CHECK (auth.uid() IS NOT NULL OR submitted_by = 'anonymous');

-- traffic_updates: public read
CREATE POLICY "Public read traffic" ON traffic_updates
  FOR SELECT USING (true);

-- city_alerts: public read active alerts
CREATE POLICY "Public read alerts" ON city_alerts
  FOR SELECT USING (active = true);

-- feed_posts: public read approved, auth users insert
CREATE POLICY "Public read feed" ON feed_posts
  FOR SELECT USING (moderated = true AND removed = false AND flagged = false);
CREATE POLICY "Users insert feed" ON feed_posts
  FOR INSERT WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "Users update own feed" ON feed_posts
  FOR UPDATE USING (auth.uid() = user_id);

-- post_likes: users manage their own likes
CREATE POLICY "Anyone can view likes" ON post_likes
  FOR SELECT USING (true);
CREATE POLICY "Authenticated users can like" ON post_likes
  FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can unlike" ON post_likes
  FOR DELETE USING (auth.uid() = user_id);

-- fare_index: public read
CREATE POLICY "Public read fare index" ON fare_index
  FOR SELECT USING (true);

-- user_profiles: public read, users manage own
CREATE POLICY "Users can view all profiles" ON user_profiles
  FOR SELECT USING (true);
CREATE POLICY "Users can update own profile" ON user_profiles
  FOR UPDATE USING (auth.uid() = id);
CREATE POLICY "Users can insert own profile" ON user_profiles
  FOR INSERT WITH CHECK (auth.uid() = id);

-- chat_logs: users read own logs
CREATE POLICY "Users read own chats" ON chat_logs
  FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users insert own chats" ON chat_logs
  FOR INSERT WITH CHECK (auth.uid() = user_id);
โ‘ฃ Enable Realtime
Enable Supabase Realtime on tables that need live updates in the browser.
1
Dashboard Method (Easiest)

Go to Database โ†’ Replication in your Supabase dashboard. Enable replication for: fare_reports, traffic_updates, feed_posts, city_alerts

OR VIA SQL
ALTER PUBLICATION supabase_realtime ADD TABLE
  fare_reports, traffic_updates, feed_posts, city_alerts;
โ‘ค Auth Configuration
Configure magic link auth so users can sign in with just their email โ€” no password needed.
1
Enable Email Auth

Go to Authentication โ†’ Providers โ†’ Email and ensure it's enabled with Magic Links turned on.

โœ“ Magic links mean users never need a password โ€” they click a link in their email to sign in. Perfect for Nigerian users on mobile.
2
Set Redirect URL

Go to Authentication โ†’ URL Configuration and add your app URL to Redirect URLs:

https://your-app.netlify.app
http://localhost:3000  โ† for local testing
โ‘ฅ Edge Functions CLI Setup
Install the Supabase CLI and link your project. Edge Functions run on Deno โ€” no Node.js config needed.
INSTALL SUPABASE CLI
# Install via npm
npm install -g supabase

# Login to Supabase
supabase login

# Initialize in your project folder
mkdir conductor-ai && cd conductor-ai
supabase init

# Link to your remote project (get project-ref from dashboard URL)
supabase link --project-ref YOUR_PROJECT_REF

# Add your OpenRouter API key as a secret
supabase secrets set OPENROUTER_API_KEY=sk-or-v1-your-key-here

# Verify secrets are set
supabase secrets list
โš ๏ธ Never put your OPENROUTER_API_KEY in frontend code. It must only live as a Supabase secret, accessed only by Edge Functions.
โ‘ฆ Traffic Agent Edge Function
Calls OpenRouter AI to generate traffic updates and saves them to the database. Browsers instantly see the update via Realtime.
supabase/functions/traffic-agent/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from "https://esm.sh/@supabase/supabase-js@2"

const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
const MODEL = "meta-llama/llama-3.1-8b-instruct:free"

serve(async (req) => {
  const supabase = createClient(
    Deno.env.get("SUPABASE_URL")!,
    Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
  )

  const now = new Date()
  const hour = now.getUTCHours() + 1 // WAT = UTC+1
  const day = now.toLocaleDateString("en-NG", { weekday: "long" })

  const prompt = `
    Current time in Nigeria: ${hour}:00 WAT, ${day}.
    Generate realistic traffic updates for Lagos, Abuja, Port Harcourt, Kano, Ibadan.
    Use real Nigerian road knowledge. For example:
    - Morning rush (7-9am): Third Mainland, Oshodi, Ojota are always severe
    - Evening rush (4-7pm): Lekki, VI, Ikorodu are always severe
    - Friday afternoons: everywhere is worse
    - Weekends: lighter traffic overall

    Respond ONLY with this JSON (no other text):
    {
      "updates": [
        {
          "city": "Lagos",
          "level": "severe",
          "alert": "Third Mainland Bridge go-slow! Avoid until 9pm",
          "routes_affected": ["Third Mainland", "Ozumba Mbadiwe"]
        }
      ]
    }
  `

  try {
    const res = await fetch(OPENROUTER_URL, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${Deno.env.get("OPENROUTER_API_KEY")}`,
        "Content-Type": "application/json",
        "HTTP-Referer": "https://conductor-ai.ng",
        "X-Title": "Conductor AI"
      },
      body: JSON.stringify({
        model: MODEL,
        max_tokens: 600,
        messages: [{ role: "user", content: prompt }]
      })
    })

    const data = await res.json()
    const text = data.choices?.[0]?.message?.content || "{}"
    const clean = text.replace(/```json\n?|\n?```/g, "").trim()
    const parsed = JSON.parse(clean)

    // Delete old traffic data (keep last 2 hours)
    await supabase
      .from("traffic_updates")
      .delete()
      .lt("created_at", new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString())

    // Insert new traffic updates
    const { error } = await supabase
      .from("traffic_updates")
      .insert(parsed.updates.map((u: any) => ({
        city: u.city,
        level: u.level,
        alert: u.alert,
        routes_affected: u.routes_affected,
        source: "ai"
      })))

    // Log the agent run
    await supabase.from("agent_runs").insert({
      agent_name: "traffic-agent",
      model: MODEL,
      tokens_used: data.usage?.total_tokens || 0,
      status: error ? "error" : "success",
      output: parsed
    })

    return new Response(
      JSON.stringify({ success: !error, updates: parsed.updates?.length }),
      { headers: { "Content-Type": "application/json" } }
    )
  } catch (err) {
    return new Response(
      JSON.stringify({ error: err.message }),
      { status: 500, headers: { "Content-Type": "application/json" } }
    )
  }
})
โ‘ง Fare Updater Edge Function
Reads recent fare reports from the DB and uses AI to update the Fare Index with averages and trends.
supabase/functions/fare-agent/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from "https://esm.sh/@supabase/supabase-js@2"

const MODEL = "google/gemma-2-9b-it:free"

serve(async () => {
  const supabase = createClient(
    Deno.env.get("SUPABASE_URL")!,
    Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
  )

  // Get recent fare reports from the last 7 days
  const { data: reports } = await supabase
    .from("fare_reports")
    .select("city, from_area, to_area, transport_mode, fare_amount, time_slot")
    .gt("created_at", new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString())
    .limit(100)

  if (!reports?.length) {
    return new Response(JSON.stringify({ message: "No recent reports" }))
  }

  const prompt = `
    You are a Nigerian transport fare analyst. Here are recent fare reports:
    ${JSON.stringify(reports)}

    Analyze these reports and identify:
    1. Routes where fares have changed significantly
    2. Which transport modes are most expensive right now
    3. Any surge patterns (rush hour, fuel scarcity, etc.)

    Respond ONLY with this JSON:
    {
      "fare_updates": [
        {
          "city": "Lagos",
          "from_area": "Lekki",
          "to_area": "Victoria Island",
          "transport_mode": "uber",
          "avg_fare": 3500,
          "min_fare": 2800,
          "max_fare": 5000,
          "trend": "up",
          "reason": "Surge due to rain"
        }
      ],
      "summary": "Brief summary of fare trends"
    }
  `

  const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${Deno.env.get("OPENROUTER_API_KEY")}`,
      "Content-Type": "application/json",
      "HTTP-Referer": "https://conductor-ai.ng"
    },
    body: JSON.stringify({ model: MODEL, max_tokens: 800, messages: [{ role: "user", content: prompt }] })
  })

  const data = await res.json()
  const text = data.choices?.[0]?.message?.content || "{}"
  const parsed = JSON.parse(text.replace(/```json\n?|\n?```/g, "").trim())

  // Upsert fare index (update or insert per route)
  for (const update of parsed.fare_updates || []) {
    await supabase.from("fare_index").upsert({
      city: update.city,
      from_area: update.from_area,
      to_area: update.to_area,
      transport_mode: update.transport_mode,
      avg_fare: update.avg_fare,
      min_fare: update.min_fare,
      max_fare: update.max_fare,
      trend: update.trend,
      last_updated: new Date().toISOString()
    }, { onConflict: "city,from_area,to_area,transport_mode" })
  }

  return new Response(
    JSON.stringify({ success: true, updated: parsed.fare_updates?.length }),
    { headers: { "Content-Type": "application/json" } }
  )
})
โ‘จ Alert Generator Edge Function
Generates city alerts and moderates community feed posts using free AI models.
supabase/functions/alerts-agent/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from "https://esm.sh/@supabase/supabase-js@2"

serve(async () => {
  const supabase = createClient(
    Deno.env.get("SUPABASE_URL")!,
    Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
  )

  // Step 1: Moderate unmoderated feed posts
  const { data: posts } = await supabase
    .from("feed_posts")
    .select("id, content")
    .eq("moderated", false)
    .limit(10)

  if (posts?.length) {
    const modPrompt = `
      Moderate these Nigerian transport app community posts.
      Remove spam, scams, profanity, and off-topic content.
      Keep road updates, fare reports, and helpful info.

      Posts: ${JSON.stringify(posts.map(p => ({ id: p.id, text: p.content })))}

      Respond ONLY with JSON:
      {"results": [{"id": 1, "approve": true, "reason": "Valid traffic update"}]}
    `

    const modRes = await fetch("https://openrouter.ai/api/v1/chat/completions", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${Deno.env.get("OPENROUTER_API_KEY")}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        model: "mistralai/mistral-7b-instruct:free",
        max_tokens: 400,
        messages: [{ role: "user", content: modPrompt }]
      })
    })

    const modData = await modRes.json()
    const modText = modData.choices?.[0]?.message?.content || "{}"
    const modResult = JSON.parse(modText.replace(/```json\n?|\n?```/g, "").trim())

    // Apply moderation decisions
    for (const result of modResult.results || []) {
      await supabase.from("feed_posts").update({
        approved: result.approve,
        moderated: true
      }).eq("id", result.id)
    }
  }

  // Step 2: Generate new city alerts
  const hour = new Date().getUTCHours() + 1
  const alertPrompt = `
    Generate 2-3 actionable road alerts for Nigerian cities at ${hour}:00 WAT.
    Be realistic based on Nigerian road patterns.

    Respond ONLY with JSON:
    {
      "alerts": [
        {
          "city": "Lagos",
          "type": "traffic",
          "severity": "warning",
          "title": "Oshodi Go-Slow",
          "body": "Heavy traffic on Oshodi-Apapa expressway. Use alternative routes.",
          "expires_hours": 3
        }
      ]
    }
  `

  const alertRes = await fetch("https://openrouter.ai/api/v1/chat/completions", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${Deno.env.get("OPENROUTER_API_KEY")}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      model: "qwen/qwen-2-7b-instruct:free",
      max_tokens: 500,
      messages: [{ role: "user", content: alertPrompt }]
    })
  })

  const alertData = await alertRes.json()
  const alertText = alertData.choices?.[0]?.message?.content || "{}"
  const alertParsed = JSON.parse(alertText.replace(/```json\n?|\n?```/g, "").trim())

  // Deactivate old alerts
  await supabase.from("city_alerts")
    .update({ active: false })
    .lt("expires_at", new Date().toISOString())

  // Insert new alerts
  await supabase.from("city_alerts").insert(
    (alertParsed.alerts || []).map((a: any) => ({
      city: a.city,
      type: a.type,
      severity: a.severity,
      title: a.title,
      body: a.body,
      active: true,
      expires_at: new Date(Date.now() + (a.expires_hours || 2) * 3600000).toISOString()
    }))
  )

  return new Response(JSON.stringify({ success: true }), {
    headers: { "Content-Type": "application/json" }
  })
})
โ‘ฉ Cron Schedule (pg_cron)
Schedule your Edge Functions to run automatically โ€” no server needed. Run this SQL in the SQL Editor.
CRON JOBS SQL
-- Traffic agent: every 30 minutes
SELECT cron.schedule(
  'traffic-agent-30min',
  '*/30 * * * *',
  $$
    SELECT net.http_post(
      url := current_setting('app.supabase_url') || '/functions/v1/traffic-agent',
      headers := jsonb_build_object(
        'X-CRON-SECRET', current_setting('app.cron_secret'),
        'Content-Type', 'application/json'
      ),
      body := '{}'::jsonb
    );
  $$
);

-- Fare agent: every 2 hours
SELECT cron.schedule(
  'fare-agent-2hr',
  '0 */2 * * *',
  $$
    SELECT net.http_post(
      url := current_setting('app.supabase_url') || '/functions/v1/fare-agent',
      headers := jsonb_build_object(
        'X-CRON-SECRET', current_setting('app.cron_secret'),
        'Content-Type', 'application/json'
      ),
      body := '{}'::jsonb
    );
  $$
);

-- Alert + moderation agent: every hour
SELECT cron.schedule(
  'alerts-agent-1hr',
  '0 * * * *',
  $$
    SELECT net.http_post(
      url := current_setting('app.supabase_url') || '/functions/v1/alerts-agent',
      headers := jsonb_build_object(
        'X-CRON-SECRET', current_setting('app.cron_secret'),
        'Content-Type', 'application/json'
      ),
      body := '{}'::jsonb
    );
  $$
);

-- Clean up old data: daily at 2am WAT (1am UTC)
SELECT cron.schedule(
  'daily-cleanup',
  '0 1 * * *',
  $$
    DELETE FROM traffic_updates WHERE created_at < now() - INTERVAL '24 hours';
    DELETE FROM city_alerts WHERE expires_at < now() - INTERVAL '1 hour';
    DELETE FROM chat_logs WHERE created_at < now() - INTERVAL '30 days';
  $$
);

-- View all scheduled jobs
SELECT * FROM cron.job;
๐Ÿ’ก pg_cron uses UTC time. Nigeria is UTC+1 (WAT). So 9am WAT = 8am UTC. Adjust cron times accordingly for Nigerian rush hours.
โ‘ช Deploy Edge Functions & Go Live
Deploy all functions and launch the app.
DEPLOY COMMANDS
# Deploy all Edge Functions at once
supabase functions deploy traffic-agent
supabase functions deploy fare-agent
supabase functions deploy alerts-agent
supabase functions deploy moderator-agent
supabase functions deploy content-agent
supabase functions deploy chat
supabase functions deploy admin-stats
supabase functions deploy admin-users
supabase functions deploy admin-fares
supabase functions deploy admin-content
supabase functions deploy admin-alerts
supabase functions deploy admin-feed

# Test a function manually
supabase functions invoke traffic-agent --no-verify-jwt

# Check function logs
supabase functions logs traffic-agent

# Set app URL and cron secret for pg_cron
# (Replace with your actual values)
ALTER DATABASE postgres SET app.supabase_url = 'https://xxxx.supabase.co';
ALTER DATABASE postgres SET app.cron_secret = 'your-long-random-secret';
๐ŸŒ
Host the HTML App (Free)

Deploy app-live.html (and the rest of the repo static files) to any static host:

# Option 1: Netlify Drop (easiest โ€” drag and drop)
# Go to netlify.com/drop and drag your HTML file

# Option 2: Vercel CLI
npm i -g vercel
vercel deploy .

# Option 3: GitHub Pages
# Push to GitHub โ†’ Settings โ†’ Pages โ†’ Deploy from main branch
โ‘ซ Final Checklist
Before sharing the app with users, verify everything is working.
  • โœ“ Supabase project created and database password saved
  • โœ“ All SQL schema tables created successfully
  • โœ“ RLS policies applied to all tables
  • โœ“ Realtime enabled for fare_reports, traffic_updates, feed_posts
  • โœ“ Auth configured with magic link enabled
  • โœ“ OpenRouter API key added as Supabase secret
  • โœ“ All 3 Edge Functions deployed successfully
  • โœ“ Cron jobs scheduled (check cron.job table)
  • โœ“ traffic-agent tested manually and returns traffic data
  • โœ“ App HTML deployed to Netlify/Vercel with public URL
  • โœ“ Supabase URL + anon key entered in app setup modal
  • โœ“ Fare report submitted โ†’ appears in fare_reports table
  • โœ“ Community post submitted โ†’ appears in feed_posts table
  • โœ“ Realtime tested: open 2 browser tabs, post in one, see it in other
โœ…
You're live! Share the URL on WhatsApp, Twitter/X, and with Nigerian transport groups. The AI agents will automatically update traffic and fares every 30 minutes.
๐Ÿ’ก Free tier capacity: Supabase free tier supports ~50,000 monthly active users, 2M Edge Function calls, and 500MB database. This is enough to grow to thousands of Nigerian users before needing to upgrade.