buddsapp

offline · self-host only

BuddsApp

A group chat I built for my friends: invite-only workspaces, real-time messages, voice rooms, stories, and an AI bot with opinions. It isn't hosted anymore. This page is about how it's wired, and what I fixed before open-sourcing it.

stack
React 19 · Vite · Supabase · LiveKit · Gemini
built
solo, for one friend group
status
source available, bring your own keys
01

What's in it

02

How it's wired

There's no custom backend server. The browser talks to Supabase directly, and anything that needs a secret goes through an Edge Function. Postgres row-level security decides what each user can read or write.

BuddsApp architecture The browser talks to Supabase for auth, data, realtime and storage. It calls two Edge Functions: livekit-token, which signs voice room tokens, and ai-chat, which forwards prompts to Gemini. The browser joins LiveKit voice rooms over WebRTC with the signed token. WebRTC audio · room token Browser React · Vite · PWA Supabase Auth (Google) Postgres + RLS Realtime Storage Edge Functions livekit-token secret ai-chat secret queries · ws invoke + JWT LiveKit Cloud voice rooms signs with LiveKit secret Gemini API Budds AI replies
Secrets live only in Edge Function environment variables. The browser only ever holds the public anon key and the user's session.
03

What I fixed before publishing

The first version worked for a group of friends who trusted each other. Before putting the code in public, I went through it as if a stranger would read the bundle. Four things didn't hold up.

  1. The LiveKit secret was in the browser

    Voice room tokens were signed client-side with VITE_LIVEKIT_API_SECRET. Anything prefixed with VITE_ ends up in the JavaScript bundle, so anyone could mint tokens for any room. Signing moved to an Edge Function that also checks the caller belongs to that chat.

    src/utils/livekitToken.js
    - const apiSecret = import.meta.env.VITE_LIVEKIT_API_SECRET;
    - const jwt = await new SignJWT({ ... }).sign(secretKey);
    + const { data } = await supabase.functions
    +   .invoke('livekit-token', {
    +     body: { roomName, participantName, participantId }
    +   });
  2. The Gemini key was readable from a table

    The AI key sat in a global_settings row that the client fetched before every request. Any signed-in user could read it. Prompts now go through an ai-chat function that holds the key, fixes the model server-side and caps the request size. The old row gets deleted by the migration.

    src/services/aiService.js
    - const { data } = await supabase.from('global_settings')
    -   .select('text_value').eq('id', 'gemini_api_key').single();
    + const { data, error } = await supabase.functions
    +   .invoke('ai-chat', { body: { contents, generationConfig } });
  3. "Admin" was an email check in the frontend

    The app compared the user's email to VITE_ADMIN_EMAIL, and a hard-coded "easter egg" password could skip the invite step. Both were visible in the bundle. Admin is now a role column, and a trigger stops users from changing their own role or workspace.

    supabase/migrations/0001_rls_policies.sql
      if current_user in ('authenticated', 'anon')
         and not public.is_admin() then
    +   new.role := old.role;
    +   new.workspace_id := old.workspace_id;
      end if;
  4. Invite codes could be listed

    The client looked up invites directly, so the table had to be readable, which meant unused codes could be listed. Invites are now admin-only, and joining happens through one security definer function that checks the code, marks it used and assigns the workspace in a single step.

    src/pages/Invite.jsx
    - await supabase.from('invites').select('*').eq('id', token)
    + await supabase.rpc('redeem_invite', { code: token })

Not verified yet: the RLS policies and the trigger are written and reviewed, but they haven't been run against a live database. The repo also doesn't include the table schema yet, so a fresh setup can't fully work until that's exported.

04

Run it yourself

You'll need free accounts on Supabase, LiveKit Cloud and Google AI Studio. The full guide is in the README.

  1. Clone and install.

    git clone https://github.com/onuracar-dev/buddsapp
    cd buddsapp && npm install
  2. Fill in .env with your public Supabase and LiveKit values.

    VITE_SUPABASE_URL=...
    VITE_SUPABASE_ANON_KEY=...
    VITE_LIVEKIT_URL=wss://your-project.livekit.cloud
  3. Store the secrets on Supabase and deploy the functions.

    supabase secrets set LIVEKIT_API_KEY=... LIVEKIT_API_SECRET=... GEMINI_API_KEY=...
    supabase functions deploy livekit-token
    supabase functions deploy ai-chat
  4. Apply supabase/migrations, optionally load supabase/seed.sql, then start the app.

    npm run dev