Skip to main content
Shipnative supports two backends: Supabase (PostgreSQL) and Convex (reactive TypeScript). This guide covers schema design, security patterns, and data management for both.
Database vs Local Storage: Store user data (profiles, preferences, content) in the database so it syncs across devices. Use MMKV only for local caching. See State Management for details.

Database Schema

When you run supabase/schema.sql, it creates core tables for common app requirements.

Core Tables

TableDescriptionKey Fields
profilesUser profile informationfirst_name, last_name, avatar_url, bio
user_preferencesApp-specific settingslanguage, timezone, profile_visibility
push_tokensDevice tokens for notificationstoken, platform, device_id, is_active
waitlistMarketing waitlistemail, source, created_at
Automatic Sync: The profiles and user_preferences tables are automatically created via triggers whenever a new user signs up.

Security

Security is built-in at the database level with Row Level Security (RLS):
  • Strict Privacy: Users can only access their own data
  • Public Profiles: Basic profile info is publicly readable for social features
  • Tokens & Preferences: Strictly private, owner-only access
-- Users can only access their own data
CREATE POLICY "Users access own data"
  ON profiles FOR ALL
  USING (auth.uid() = id);

-- Public read, authenticated write
CREATE POLICY "Anyone can read posts"
  ON posts FOR SELECT USING (true);

CREATE POLICY "Users create own posts"
  ON posts FOR INSERT
  WITH CHECK (auth.uid() = author_id);
Always enable RLS: When creating new tables, run ALTER TABLE name ENABLE ROW LEVEL SECURITY; and add policies.

Managing the Schema

Shipnative uses database migrations to version control schema changes. Always use migrations instead of editing schema.sql directly.Migrations provide version control and prevent “already exists” errors when running the schema multiple times—the industry standard used by Rails, Django, Laravel, and Prisma.

Create a New Migration

# Create a new migration file
supabase migration new add_todos_table

# This creates: supabase/migrations/YYYYMMDDHHMMSS_add_todos_table.sql

Apply Migrations

# Link your project (one-time)
supabase link --project-ref your-project-ref

# Apply all migrations
supabase db push

Migration Best Practices

  1. Always use IF NOT EXISTS - Makes migrations safe to run multiple times
  2. Drop policies before recreating - Prevents “already exists” errors
  3. Enable RLS on all user tables - Security best practice
  4. Add indexes for foreign keys - Performance optimization
Example migration:
-- Create table
CREATE TABLE IF NOT EXISTS public.todos (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
    title TEXT NOT NULL,
    completed BOOLEAN DEFAULT false,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Enable RLS
ALTER TABLE public.todos ENABLE ROW LEVEL SECURITY;

-- Add policies
DROP POLICY IF EXISTS "Users can view own todos" ON public.todos;
CREATE POLICY "Users can view own todos"
    ON public.todos FOR SELECT
    USING (auth.uid() = user_id);

-- Add indexes
CREATE INDEX IF NOT EXISTS todos_user_id_idx ON public.todos(user_id);
See the complete migration guide at supabase/migrations/README.md in your project.
You can also run SQL directly in the Supabase SQL Editor, but this won’t be version controlled:
ALTER TABLE public.[table_name] ADD COLUMN [column_name] [type] DEFAULT [value];

Seed Data

Use the SQL Editor to insert test data:
INSERT INTO profiles (id, first_name, last_name)
VALUES ('user-id-here', 'Demo', 'User');
Or import from a file:
npx supabase db seed

Reference Implementation: DataDemoScreen

The boilerplate includes a DataDemoScreen that demonstrates proper data fetching patterns for your chosen backend. Use it as a template when building your own data-driven screens.

How It Works

The screen uses conditional exports to load the correct implementation:
screens/
├── DataDemoScreen.tsx           # Router - loads correct version
├── DataDemoScreen.supabase.tsx  # Supabase patterns
└── DataDemoScreen.convex.tsx    # Convex patterns
The main DataDemoScreen.tsx automatically exports the right version based on your EXPO_PUBLIC_BACKEND_PROVIDER setting:
import { isConvex } from "@/config/env"

export const DataDemoScreen = isConvex
  ? require("./DataDemoScreen.convex").DataDemoScreen
  : require("./DataDemoScreen.supabase").DataDemoScreen

What Each Version Demonstrates

File: screens/DataDemoScreen.supabase.tsxShows the standard React Query + Supabase SDK pattern:
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { supabase } from "@/services/supabase"

// Fetching data
const usePosts = () => {
  return useQuery({
    queryKey: ["posts"],
    queryFn: async () => {
      const { data, error } = await supabase
        .from("posts")
        .select("*")
        .order("created_at", { ascending: false })

      if (error) throw error
      return data
    },
  })
}

// Creating data with optimistic updates
const useCreatePost = () => {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: async (newPost) => {
      const { data, error } = await supabase
        .from("posts")
        .insert(newPost)
        .select()
        .single()

      if (error) throw error
      return data
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["posts"] })
    },
  })
}
Key patterns:
  • React Query for caching and state management
  • Manual cache invalidation after mutations
  • Pull-to-refresh for manual data refresh
  • Optimistic updates for better UX

Using the Demo Screen

Navigate to the DataDemoScreen from any authenticated screen:
navigation.navigate('DataDemo')
Copy the patterns, not the screen. The DataDemoScreen is meant to be a reference. When building your own screens, look at the version matching your backend (.supabase.tsx or .convex.tsx) and copy the data fetching patterns.

Mock Database

In development without backend credentials, the app uses a mock database:
  • In-memory storage: Data persists during your session
  • Secure storage fallback: Critical data (like auth) persists across restarts
  • API compatibility: Same API as the real backend
See the Mock Services guide for details.

Next Steps