weapon 0.1.1

Cross-device sync engine
Documentation
# Syncing with Supabase

This guide covers everything needed to set up Supabase for syncing with Weapon, including the database schema, RPC functions, and security policies.

## Prerequisites

- A Supabase project
- Authentication enabled (Supabase Auth)
- Real-time subscriptions enabled (for instant sync)

## Database Schema

### Events Table

The core table that stores all events from all devices:

```sql
-- Create the events table
create table events (
  id bigserial primary key,
  user_id uuid references auth.users,
  stream_id text not null,
  device_id text not null,
  within_device_events_index integer not null,
  event jsonb not null,
  created_at timestamptz default now()
);

-- Create indexes for efficient querying
create index idx_events_sync on events(device_id, id);
create index idx_events_stream_sync on events(stream_id, device_id, id);

-- Create unique constraint to prevent duplicate events
alter table events 
add constraint events_unique_stream_device_index 
unique (user_id, stream_id, device_id, within_device_events_index);

-- Enable Row Level Security
alter table events enable row level security;

-- Create RLS policies
create policy "Users can see own events" on events
  for select using (auth.uid() = user_id);

create policy "Users can insert own events" on events
  for insert with check (auth.uid() = user_id);

create policy "Users can update own events" on events
  for update using (auth.uid() = user_id);
```

### Column Descriptions

- `id`: Auto-incrementing primary key
- `user_id`: References the authenticated user
- `stream_id`: Identifies which data stream (e.g., "reviews", "deck_selection")
- `device_id`: Unique identifier for the device that created the event
- `within_device_events_index`: Sequential index for events from a specific device
- `event`: The actual event data in JSONB format
- `created_at`: Timestamp when the event was stored in Supabase

## RPC Functions

### 1. sync_events Function

This is the main synchronization function that handles bidirectional sync:

```sql
create or replace function sync_events(sync_request jsonb)
returns jsonb as $$
declare
  result jsonb = '{}'::jsonb;
  stream_record record;
  device_record record;
  stream_result jsonb;
  requested_devices jsonb;
begin
  -- Loop through each stream in the request
  for stream_record in select * from jsonb_each(sync_request)
  loop
    stream_result := '{}'::jsonb;
    requested_devices := stream_record.value->'last_synced_ids';
    
    -- First, get events for explicitly requested devices
    for device_record in select * from jsonb_each_text(requested_devices)
    loop
      stream_result := stream_result || jsonb_build_object(
        device_record.key,
        (
          select coalesce(jsonb_agg(
            row_to_json(e) order by e.within_device_events_index
          ), '[]'::jsonb)
          from events e
          where e.device_id = device_record.key
          and e.stream_id = stream_record.key
          and e.within_device_events_index > device_record.value::integer
          and e.user_id = auth.uid()
        )
      );
    end loop;
    
    -- Then, get ALL events for devices not in the request
    for device_record in 
      select distinct device_id 
      from events 
      where stream_id = stream_record.key 
      and user_id = auth.uid()
      and device_id not in (select jsonb_object_keys(requested_devices))
    loop
      stream_result := stream_result || jsonb_build_object(
        device_record.device_id,
        (
          select coalesce(jsonb_agg(
            row_to_json(e) order by e.within_device_events_index
          ), '[]'::jsonb)
          from events e
          where e.device_id = device_record.device_id
          and e.stream_id = stream_record.key
          and e.user_id = auth.uid()
        )
      );
    end loop;
    
    -- Add this stream's results to the main result
    result := result || jsonb_build_object(stream_record.key, stream_result);
  end loop;
  
  return result;
end;
$$ language plpgsql
set search_path = public, auth, extensions, pg_catalog;

-- Grant execution rights
grant execute on function sync_events(jsonb) to authenticated;
```

#### Input Format:
```json
{
  "stream_id": {
    "last_synced_ids": {
      "device_id": last_within_device_events_index
    }
  }
}
```

#### Output Format:
```json
{
  "stream_id": {
    "device_id": [array_of_events]
  }
}
```

### 2. get_clock Function

Returns the current event count per device per stream:

```sql
create or replace function public.get_clock(p_user_id uuid)
returns jsonb
language sql
stable
set search_path = public
as $$
  with counts as (
    select
      stream_id,
      device_id,
      count(*)::int as event_count
    from public.events
    where user_id = p_user_id
    group by stream_id, device_id
  ),
  device_map as (
    select
      stream_id,
      jsonb_object_agg(device_id::text, to_jsonb(event_count)) as devices
    from counts
    group by stream_id
  )
  select coalesce(
    jsonb_object_agg(stream_id::text, devices),
    '{}'::jsonb
  )
  from device_map;
$$;

-- Grant execution rights
grant execute on function public.get_clock(uuid) to authenticated, service_role;
```

#### Output Format:
```json
{
  "stream_id": {
    "device_id": event_count
  }
}
```

## Real-time Subscriptions

Enable real-time for instant cross-device sync:

```sql
-- Enable real-time for the events table
alter publication supabase_realtime add table events;
```

## Usage in Weapon

### JavaScript/TypeScript Client Setup

```typescript
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

// Subscribe to real-time events
const channel = supabase
  .channel(`events:${userId}`)
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'events',
      filter: `user_id=eq.${userId}`,
    },
    (payload) => {
      const { device_id, stream_id, event } = payload.new
      // Handle incoming event from another device
      weapon.add_remote_event(device_id, stream_id, event)
    }
  )
  .subscribe()
```

### Rust/WASM Integration

The Weapon library handles the sync protocol internally. When using the Supabase feature:

```rust
// In your Cargo.toml
[dependencies]
weapon = { features = ["supabase"] }

// In your code
use weapon::supabase::sync_with_supabase;

// Sync with Supabase
async fn sync(access_token: &str) {
    // Get local event state
    let clock = weapon.get_clock();
    
    // Call Supabase RPC
    let response = supabase_client
        .rpc("sync_events", clock)
        .execute()
        .await?;
    
    // Apply remote events
    weapon.apply_remote_events(response);
    
    // Upload local events
    let local_events = weapon.get_unsent_events();
    for event in local_events {
        supabase_client
            .from("events")
            .insert(event)
            .execute()
            .await?;
    }
}
```

## Security Considerations

1. **Row Level Security (RLS)**: Always enabled to ensure users can only access their own events
2. **Unique Constraints**: Prevent duplicate events with the composite unique constraint
3. **Authentication Required**: All operations require a valid authenticated user