Skip to main content
Raffi’s backend is built on a modern serverless stack, combining Convex for real-time features and Supabase for authentication and relational data.

Tech Stack Overview

Backend Services

┌─────────────────────────────────────────────────────────┐
│                    Raffi Backend                        │
│                                                         │
│  ┌────────────────────┐      ┌────────────────────┐   │
│  │      Convex        │      │     Supabase       │   │
│  │   (Real-time DB)   │      │   (PostgreSQL)     │   │
│  │                    │      │                    │   │
│  │  - Watch Parties   │      │  - Authentication  │   │
│  │  - Lists           │      │  - User Profiles   │   │
│  │  - Library Sync    │      │  - Storage         │   │
│  │  - Live Queries    │      │  - Row Security    │   │
│  │  - Mutations       │      │                    │   │
│  └────────────────────┘      └────────────────────┘   │
│           ↕                           ↕                │
│  ┌─────────────────────────────────────────────────┐  │
│  │              Client Apps                        │  │
│  │  - Desktop (Electron)                           │  │
│  │  - Mobile (React Native)                        │  │
│  └─────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

Technology Choices

  • Convex 1.31.7: Real-time serverless backend
  • Supabase 2.95.3: PostgreSQL database with real-time capabilities
  • Ave ID SDK 0.2.2: Authentication provider
  • TypeScript: Shared types across frontend and backend

Convex Backend

Overview

Convex provides the real-time layer for collaborative features and synchronized state across devices. Location: convex/ directory in root
convex/
├── schema.ts              # Database schema
├── raffi.ts              # Main backend logic (53KB)
├── auth.config.ts         # Authentication config
└── _generated/           # Auto-generated types

Database Schema

Location: convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  // User addons (Stremio addon ecosystem)
  addons: defineTable({
    user_id: v.string(),
    added_at: v.string(),
    transport_url: v.string(),
    manifest: v.any(),
    flags: v.optional(v.any()),
    addon_id: v.string(),
  })
    .index("by_user", ["user_id"])
    .index("by_user_transport", ["user_id", "transport_url"]),

  // User library (watched content)
  libraries: defineTable({
    user_id: v.string(),
    imdb_id: v.string(),
    progress: v.any(),           // { season, episode, time, etc }
    last_watched: v.string(),
    completed_at: v.optional(v.union(v.string(), v.null())),
    type: v.string(),            // "movie" | "series"
    shown: v.boolean(),          // Visibility in library
    poster: v.optional(v.string()),
  })
    .index("by_user", ["user_id"])
    .index("by_user_imdb", ["user_id", "imdb_id"]),

  // Custom lists
  lists: defineTable({
    list_id: v.string(),
    user_id: v.string(),
    created_at: v.string(),
    name: v.string(),
    position: v.number(),        // Display order
  })
    .index("by_user", ["user_id"])
    .index("by_list_id", ["list_id"]),

  // Items in custom lists
  list_items: defineTable({
    list_id: v.string(),
    imdb_id: v.string(),
    position: v.number(),
    type: v.string(),
    poster: v.optional(v.string()),
  })
    .index("by_list", ["list_id"])
    .index("by_list_imdb", ["list_id", "imdb_id"]),

  // Watch parties (synchronized viewing)
  watch_parties: defineTable({
    party_id: v.string(),
    host_user_id: v.string(),
    imdb_id: v.string(),
    season: v.optional(v.union(v.number(), v.null())),
    episode: v.optional(v.union(v.number(), v.null())),
    stream_source: v.string(),
    file_idx: v.optional(v.union(v.number(), v.null())),
    created_at: v.string(),
    expires_at: v.string(),
    current_time_seconds: v.number(),
    is_playing: v.boolean(),
    last_update: v.string(),
  })
    .index("by_party_id", ["party_id"])
    .index("by_host", ["host_user_id"]),

  // Watch party members
  watch_party_members: defineTable({
    party_id: v.string(),
    user_id: v.string(),
    joined_at: v.string(),
    last_seen: v.string(),
  })
    .index("by_party", ["party_id"])
    .index("by_party_user", ["party_id", "user_id"]),

  // Trakt.tv integration
  trakt_integrations: defineTable({
    user_id: v.string(),
    username: v.optional(v.string()),
    slug: v.optional(v.string()),
    access_token: v.string(),
    refresh_token: v.string(),
    scope: v.optional(v.string()),
    token_type: v.optional(v.string()),
    expires_at: v.optional(v.number()),
    created_at: v.string(),
    updated_at: v.string(),
  }).index("by_user", ["user_id"]),
});

Key Features

1. Addon Management

Schema: addons table Stores user’s installed Stremio addons:
  • Transport URL (addon endpoint)
  • Manifest (capabilities, name, description)
  • Flags (user-specific settings)
Indexes:
  • by_user: Get all addons for a user
  • by_user_transport: Prevent duplicate addon installations

2. Library & Progress Tracking

Schema: libraries table Tracks watched content across devices:
{
  user_id: "user123",
  imdb_id: "tt1234567",
  progress: {
    season: 2,
    episode: 5,
    time: 1234.5,      // seconds
    duration: 2400.0
  },
  last_watched: "2026-03-03T12:00:00Z",
  type: "series",
  shown: true,         // Visible in library
  poster: "https://..."
}
Indexes:
  • by_user: Get user’s entire library
  • by_user_imdb: Check if content is in library

3. Custom Lists

Schema: lists + list_items tables Two-table design for flexibility: Lists:
{
  list_id: "list-uuid",
  user_id: "user123",
  name: "Watchlist",
  position: 0,         // Display order
  created_at: "2026-03-03T12:00:00Z"
}
List Items:
{
  list_id: "list-uuid",
  imdb_id: "tt1234567",
  position: 0,         // Order within list
  type: "movie",
  poster: "https://..."
}

4. Watch Parties

Schema: watch_parties + watch_party_members tables Real-time synchronized viewing: Party State:
{
  party_id: "party-uuid",
  host_user_id: "user123",
  imdb_id: "tt1234567",
  season: 1,
  episode: 1,
  stream_source: "magnet:...",
  current_time_seconds: 1234.5,
  is_playing: true,
  last_update: "2026-03-03T12:00:00Z"
}
Members:
{
  party_id: "party-uuid",
  user_id: "user456",
  joined_at: "2026-03-03T12:00:00Z",
  last_seen: "2026-03-03T12:05:00Z"  // For presence
}
Real-time Features:
  • Host controls playback (play/pause/seek)
  • Members receive instant updates
  • Presence tracking via last_seen
  • Automatic expiration

5. Trakt Integration

Schema: trakt_integrations table Stores OAuth tokens for Trakt.tv:
  • Access token for API requests
  • Refresh token for renewal
  • Token expiration tracking

Backend Logic

Location: convex/raffi.ts (53KB file) Convex functions are written in TypeScript: Query Example (read data):
export const getLibrary = query({
  args: { userId: v.string() },
  handler: async (ctx, args) => {
    return await ctx.db
      .query("libraries")
      .withIndex("by_user", (q) => q.eq("user_id", args.userId))
      .collect();
  },
});
Mutation Example (write data):
export const updateProgress = mutation({
  args: {
    userId: v.string(),
    imdbId: v.string(),
    progress: v.any(),
  },
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("libraries")
      .withIndex("by_user_imdb", (q) =>
        q.eq("user_id", args.userId).eq("imdb_id", args.imdbId)
      )
      .first();
    
    if (existing) {
      await ctx.db.patch(existing._id, {
        progress: args.progress,
        last_watched: new Date().toISOString(),
      });
    } else {
      await ctx.db.insert("libraries", {
        user_id: args.userId,
        imdb_id: args.imdbId,
        progress: args.progress,
        last_watched: new Date().toISOString(),
        type: "movie",
        shown: true,
      });
    }
  },
});

Authentication

Location: convex/auth.config.ts
export default {
  providers: [
    {
      domain: process.env.CONVEX_SITE_URL,
      applicationID: "convex",
    },
  ],
};
Integrates with Convex Auth system for user identity.

Client Integration

Desktop

Location: raffi-desktop/src/lib/
import { ConvexClient } from "convex/browser";

const convex = new ConvexClient(import.meta.env.VITE_CONVEX_URL);

// Subscribe to real-time data
const library = convex.query(api.raffi.getLibrary, {
  userId: currentUser.id,
});

// Update data
await convex.mutation(api.raffi.updateProgress, {
  userId: currentUser.id,
  imdbId: "tt1234567",
  progress: { time: 1234.5 },
});

Mobile

Location: raffi-mobile/lib/convex.ts Same API as desktop:
import { useQuery, useMutation } from "convex/react";

function LibraryScreen() {
  const library = useQuery(api.raffi.getLibrary, {
    userId: user.id,
  });
  
  // library updates automatically when data changes
}

Supabase Backend

Overview

Supabase provides PostgreSQL database with authentication and storage. Primary Use Cases:
  1. User authentication (OAuth, email/password)
  2. User profiles and settings
  3. Fallback for non-real-time data
  4. File storage (avatars, etc.)

Client Integration

Desktop

Location: raffi-desktop/src/lib/db/
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY
);

// Authentication
await supabase.auth.signInWithOAuth({
  provider: 'google',
});

// Get current user
const { data: { user } } = await supabase.auth.getUser();

Mobile

Same SDK, different entry point:
import AsyncStorage from '@react-native-async-storage/async-storage';

const supabase = createClient(
  SUPABASE_URL,
  SUPABASE_ANON_KEY,
  {
    auth: {
      storage: AsyncStorage,  // Persist auth state
    },
  }
);

Authentication Flow

User → Click "Sign In"

Supabase OAuth (Google, GitHub, etc.)

Callback with tokens

Store in AsyncStorage (mobile) / localStorage (desktop)

Convex recognizes user via Supabase JWT

App loads user-specific data

Row-Level Security

Supabase supports RLS policies for data isolation:
-- Example policy
CREATE POLICY "Users can only access their own data"
ON user_profiles
FOR SELECT
USING (auth.uid() = user_id);

Integration with Ave ID

Package: @ave-id/sdk Ave ID provides additional authentication options:
  • Web3 wallet authentication
  • Decentralized identity
  • Cross-platform user profiles
Integration Points:
  • Desktop: raffi-desktop/package.json:134
  • Mobile: raffi-mobile/package.json:14

Data Flow Architecture

Watch Progress Sync

User watches video on Desktop

  Update local state

  Convex mutation (updateProgress)

  Real-time propagation

Mobile app receives update

  "Continue Watching" synced

Watch Party Flow

Host creates party (Convex mutation)

  Generates party_id

  Share party_id with friends

Members join (insert watch_party_members)

Host plays video

Update party state every 1s

Members subscribe to party

Receive real-time playback updates

Sync player position

Addon Sync

User adds addon on Desktop

  Insert into addons table

  Real-time sync to Convex

Mobile queries addons

  Same addon available

  Unified content sources

Performance Considerations

Real-time Subscriptions

Convex:
  • Automatic query invalidation
  • Efficient WebSocket protocol
  • Batched updates
  • Client-side caching
Best Practices:
// ✅ Good: Specific queries
const library = useQuery(api.raffi.getLibrary, { userId });

// ❌ Bad: Overfetching
const allData = useQuery(api.raffi.getAllData);

Optimistic Updates

// Update UI immediately
setLocalProgress(newProgress);

// Then sync to backend
await convex.mutation(api.raffi.updateProgress, {
  progress: newProgress,
});

Offline Support

Convex handles offline automatically:
  1. Mutations queued when offline
  2. Retry on reconnection
  3. Conflict resolution via timestamps

Deployment

Convex

# Deploy backend
cd convex
npx convex deploy

# Get deployment URL
# Set as VITE_CONVEX_URL in client apps

Supabase

Hosted:
  • Use Supabase cloud (supabase.com)
  • Get project URL and anon key
  • Set in environment variables
Self-hosted:
# Docker-based deployment
git clone https://github.com/supabase/supabase
cd supabase/docker
cp .env.example .env
docker-compose up

Security

API Keys

Environment Variables:
# Convex
VITE_CONVEX_URL=https://xxx.convex.cloud

# Supabase
VITE_SUPABASE_URL=https://xxx.supabase.co
VITE_SUPABASE_ANON_KEY=xxx
Never commit:
  • .env files
  • API keys in code
  • User tokens

Data Validation

Convex validates all inputs:
export const updateProgress = mutation({
  args: {
    userId: v.string(),    // Type-safe validation
    imdbId: v.string(),
    progress: v.object({
      time: v.number(),
      duration: v.number(),
    }),
  },
  // ...
});

Authentication

All Convex queries/mutations require authentication:
handler: async (ctx, args) => {
  const user = await ctx.auth.getUserIdentity();
  if (!user) throw new Error("Unauthenticated");
  
  // Verify user owns the data
  if (args.userId !== user.subject) {
    throw new Error("Unauthorized");
  }
  // ...
}

Monitoring & Debugging

Convex Dashboard

  • View all tables and data
  • Monitor function calls
  • Check error logs
  • Inspect WebSocket connections

Supabase Dashboard

  • Database explorer
  • Auth user management
  • Storage file browser
  • SQL editor
  • Real-time logs

Key Design Decisions

Why Convex?

  • Real-time First: Built for live data
  • Type Safety: Full TypeScript support
  • Serverless: No infrastructure management
  • Developer Experience: Excellent local development

Why Supabase?

  • PostgreSQL: Powerful relational database
  • Authentication: OAuth providers built-in
  • Open Source: Self-hosting option
  • Ecosystem: Large community and tools

Why Both?

  • Convex: Real-time collaborative features
  • Supabase: Authentication and relational data
  • Separation of Concerns: Each service excels at its role

Future Considerations

Potential Enhancements

  1. Caching Layer: Redis for frequently accessed data
  2. CDN: Serve static content (posters, metadata)
  3. Analytics: Track usage patterns
  4. Search: Algolia or Meilisearch for content discovery
  5. Recommendations: ML-based content suggestions

Build docs developers (and LLMs) love