Skip to main content

Overview

Cajas uses PostgreSQL (via Supabase) with Row Level Security (RLS) enabled on all tables. The schema supports user management, case opening, inventory tracking, and provably fair gaming verification.

Entity Relationship Diagram

┌─────────────────┐
│   auth.users    │ (Supabase Auth)
│─────────────────│
│ id: uuid PK     │
└─────────────────┘

        │ 1:1

┌─────────────────┐         ┌─────────────────┐
│    profiles     │         │   admin_logs    │
│─────────────────│         │─────────────────│
│ id: uuid PK/FK  │←───────│ admin_id: uuid  │
│ full_name       │         │ action: text    │
│ avatar_url      │         │ details: jsonb  │
│ dni             │         │ created_at      │
│ phone           │         └─────────────────┘
│ address         │
│ role            │
│ updated_at      │
└─────────────────┘

┌─────────────────┐         ┌─────────────────┐
│     users       │         │   user_seeds    │
│─────────────────│         │─────────────────│
│ id: uuid PK/FK  │←───1:1─→│ user_id: uuid PK│
│ username        │         │ server_seed     │
│ avatar_url      │         │ client_seed     │
│ balance         │         │ nonce: bigint   │
│ created_at      │         │ created_at      │
└─────────────────┘         │ updated_at      │
        │                   └─────────────────┘

        │ 1:N

┌─────────────────┐         ┌─────────────────┐
│  transactions   │         │   game_rolls    │
│─────────────────│         │─────────────────│
│ id: uuid PK     │         │ id: uuid PK     │
│ user_id: uuid FK│         │ user_id: uuid FK│
│ amount: numeric │         │ case_id: uuid FK│
│ type: text      │         │ server_seed     │
│ reference_id    │         │ client_seed     │
│ created_at      │         │ nonce: bigint   │
└─────────────────┘         │ roll_result     │
                            │ item_won_id: FK │
                            │ created_at      │
                            └─────────────────┘


    ┌───────────────────────────────┘


┌─────────────────┐         ┌─────────────────┐
│     cases       │         │   case_items    │
│─────────────────│         │─────────────────│
│ id: uuid PK     │←───1:N─→│ id: uuid PK     │
│ name: text      │         │ case_id: uuid FK│
│ slug: text      │         │ name: text      │
│ description     │         │ value: numeric  │
│ price: numeric  │         │ image_url: text │
│ image_url       │         │ probability     │
│ created_at      │         │ created_at      │
└─────────────────┘         └─────────────────┘


    ┌───────────────────────────────┘


┌─────────────────┐         ┌─────────────────┐
│     items       │         │   user_items    │
│─────────────────│         │─────────────────│
│ id: uuid PK     │←───1:N─→│ id: uuid PK     │
│ name: text      │         │ user_id: uuid FK│
│ image_url       │         │ item_id: uuid FK│
│ rarity: text    │         │ status: text    │
│ price: numeric  │         │ created_at      │
│ created_at      │         └─────────────────┘
└─────────────────┘

Tables

profiles

Extended user profile information. One-to-one with auth.users.
ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, REFERENCES auth.usersUser ID from Supabase Auth
updated_attimestamptznullableLast profile update
full_nametextnullableUser’s full name
avatar_urltextnullableProfile picture URL
dnitextnullableNational ID document
phonetextnullablePhone number
addresstextnullablePhysical address
roletextDEFAULT ‘user’, CHECKUser role: ‘user’ or ‘admin’
RLS Policies:
  • Public read: Anyone can view profiles
  • Users can insert their own profile
  • Users can update their own profile

users

Public user information for the gaming platform.
ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, REFERENCES auth.usersUser ID
usernametextUNIQUE, nullableDisplay username
avatar_urltextnullableAvatar image URL
balancenumericDEFAULT 0Virtual currency balance
client_seedtextnullableDeprecated: moved to user_seeds
nonceintegerDEFAULT 0Deprecated: moved to user_seeds
created_attimestamptzDEFAULT now()Account creation timestamp
RLS Policies:
  • Public profiles are viewable by everyone
  • Users can insert their own profile (via trigger)
  • Users can update their own profile
Database Trigger:
CREATE FUNCTION public.handle_new_user()
RETURNS trigger AS $$
BEGIN
  INSERT INTO public.users (id, username, avatar_url)
  VALUES (
    new.id,
    new.raw_user_meta_data ->> 'full_name',
    new.raw_user_meta_data ->> 'avatar_url'
  );
  RETURN new;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

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

cases

Available cases that users can open.
ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Unique case ID
nametextNOT NULLCase display name
slugtextUNIQUE, NOT NULLURL-friendly identifier
descriptiontextnullableCase description
pricenumericNOT NULL, CHECK (price >= 0)Cost to open case
image_urltextNOT NULLCase image URL
created_attimestamptzDEFAULT now()Creation timestamp
RLS Policies:
  • Public read: All users can view cases
  • Admins insert: Only admins can create cases
  • Admins update: Only admins can modify cases
  • Admins delete: Only admins can delete cases

case_items

Items that can be won from each case.
ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Unique item ID
case_iduuidREFERENCES cases ON DELETE CASCADEParent case
nametextNOT NULLItem name
valuenumericNOT NULL, CHECK (value >= 0)Item monetary value
image_urltextNOT NULLItem image URL
probabilitynumericNOT NULL, CHECK (probability greater than 0 AND less than or equal to 100)Drop chance percentage
created_attimestamptzDEFAULT now()Creation timestamp
RLS Policies:
  • Public read: All users can view items
  • Admins insert/update/delete: Only admins can modify
Usage Example:
const { data: caseItems } = await supabase
  .from('case_items')
  .select('*')
  .eq('case_id', caseId)
  .order('probability', { ascending: false })

items

Master list of all unique items in the system.
ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Unique item ID
nametextNOT NULLItem name
image_urltextnullableItem image URL
raritytextNOT NULLRarity tier: common, rare, epic, legendary
pricenumericNOT NULLItem base value
created_attimestamptzDEFAULT now()Creation timestamp
RLS Policies:
  • Public read: All users can view items

user_items

User inventory - items won from opening cases.
ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Unique record ID
user_iduuidREFERENCES users NOT NULLItem owner
item_iduuidREFERENCES items NOT NULLItem reference
statustextDEFAULT ‘in_inventory’Status: in_inventory, sold, withdrawn
created_attimestamptzDEFAULT now()When item was won
RLS Policies:
  • Users can view only their own items
Usage Example:
const { data: inventory } = await supabase
  .from('user_items')
  .select(`
    *,
    items:item_id (
      name,
      image_url,
      rarity,
      price
    )
  `)
  .eq('user_id', userId)
  .eq('status', 'in_inventory')

transactions

Financial transaction history.
ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Transaction ID
user_iduuidREFERENCES users NOT NULLUser performing transaction
amountnumericNOT NULLTransaction amount
typetextNOT NULLType: deposit, withdraw, case_open, item_sell
reference_iduuidnullableLink to case or item
created_attimestamptzDEFAULT now()Transaction timestamp
RLS Policies:
  • Users can view only their own transactions

user_seeds

Provably fair seed pairs for each user.
ColumnTypeConstraintsDescription
user_iduuidPRIMARY KEY, REFERENCES auth.usersUser ID
server_seedtextNOT NULLServer-generated secret seed
client_seedtextNOT NULLUser-provided or client-generated seed
noncebigintNOT NULL, DEFAULT 0Incrementing counter for each game
created_attimestamptzDEFAULT now()Initial seed creation
updated_attimestamptzDEFAULT now()Last seed update
RLS Policies:
  • Users can view their own seeds
  • Users can update their own seeds
  • Users can insert their own seeds
Usage Example:
const { data: seeds } = await supabase
  .from('user_seeds')
  .select('*')
  .eq('user_id', user.id)
  .single()

game_rolls

Audit log of all game rolls for provably fair verification.
ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Roll ID
user_iduuidREFERENCES auth.users NOT NULLPlayer
case_iduuidREFERENCES cases NOT NULLCase opened
server_seedtextNOT NULLServer seed used for this roll
client_seedtextNOT NULLClient seed used for this roll
noncebigintNOT NULLNonce value for this roll
roll_resultbigintNOT NULLRaw random number generated
item_won_iduuidREFERENCES items NOT NULLItem won
created_attimestamptzDEFAULT now()Roll timestamp
RLS Policies:
  • Users can view their own rolls
Usage Example:
// Record a game roll
await supabase.from('game_rolls').insert({
  user_id: user.id,
  case_id: caseId,
  server_seed: seeds.server_seed,
  client_seed: seeds.client_seed,
  nonce: nonce,
  roll_result: Math.floor(rollValue * 1000000),
  item_won_id: winnerItem.id
})

admin_logs

Audit trail of admin actions.
ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Log ID
admin_iduuidREFERENCES auth.users, nullableAdmin user
actiontextNOT NULLAction performed
detailsjsonbnullableAdditional action details
created_attimestamptzDEFAULT now()Action timestamp
RLS Policies:
  • Admins can view logs
  • Admins can insert logs

Database Usage Examples

Fetching Cases with Items

import { createClient } from '@/lib/supabase/server'

const supabase = await createClient()

const { data: cases, error } = await supabase
  .from('cases')
  .select(`
    *,
    case_items (
      id,
      name,
      value,
      image_url,
      probability
    )
  `)
  .order('created_at', { ascending: false })

Creating a Case (Admin Only)

const { data: newCase, error } = await supabase
  .from('cases')
  .insert({
    name: 'Premium Case',
    slug: 'premium-case',
    description: 'High-value items',
    price: 100,
    image_url: 'https://example.com/case.png'
  })
  .select()
  .single()

if (newCase) {
  // Add items to the case
  await supabase
    .from('case_items')
    .insert([
      {
        case_id: newCase.id,
        name: 'Rare Item',
        value: 500,
        image_url: 'https://example.com/item.png',
        probability: 5
      }
    ])
}

Checking User Balance

const { data: user } = await supabase
  .from('users')
  .select('balance')
  .eq('id', userId)
  .single()

if (user && user.balance >= casePrice) {
  // User can afford to open case
}

Type Safety

All database types are auto-generated in types/supabase.ts:
import { Database } from '@/types/supabase'

type Case = Database['public']['Tables']['cases']['Row']
type CaseInsert = Database['public']['Tables']['cases']['Insert']
type CaseUpdate = Database['public']['Tables']['cases']['Update']

Migrations

Database schema is managed through migration files in supabase/migrations/:
  • 20240101000000_init.sql - Initial schema with users, cases, items, transactions
  • 0000_create_cases_system.sql - Cases and admin system
  • 20251205120000_fix_case_items_schema.sql - Case items schema update
  • 20251209000000_create_provably_fair.sql - Provably fair system tables

Build docs developers (and LLMs) love