Skip to main content

Overview

Auction Excellence uses PostgreSQL with 35 tables organized into logical domains. All data access is controlled by Row Level Security (RLS) policies that enforce multi-tenant isolation.

35 Tables

Core data, chat, reporting, and audit tables

8 Views

Pre-built queries for common operations

35+ Functions

RPC functions for complex operations

Multi-Tenant Architecture

All core tables include an auction_id column that enables tenant isolation. RLS policies automatically filter data based on the authenticated user’s auction memberships.
-- Example: Locations are scoped to auctions
CREATE TABLE locations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  auction_id UUID NOT NULL REFERENCES auctions(id),
  name TEXT NOT NULL,
  -- ... other columns
);

-- RLS policy ensures users only see their auction's locations
CREATE POLICY "Users can view their auction's locations"
  ON locations FOR SELECT
  USING (is_auction_member(auction_id));

Authorization Hierarchy

The system uses a hierarchical role structure for access control:
RoleScopeCapabilities
Super AdminSystem-wideAccess all auctions, create new auctions, manage all users
OwnerPer-auctionFull auction control, transfer ownership, delete auction
AdminPer-auctionManage members, locations, and most settings
Team MemberPer-auctionSubmit data for assigned locations, participate in chat

Table Groups

User & Authentication Tables

users

Extends Supabase Auth with application-specific profile data.
ColumnTypeDescription
idUUIDPrimary key (matches auth.users.id)
emailTEXTUser’s email address
full_nameTEXTDisplay name
avatar_urlTEXTURL to profile photo in avatars storage bucket (nullable)
is_activeBOOLEANAccount status
last_auction_idUUIDLast accessed auction
created_atTIMESTAMPTZAccount creation time
updated_atTIMESTAMPTZLast profile update
Indexes:
  • idx_users_is_active on (is_active)
  • idx_users_last_auction_id on (last_auction_id)
RLS Policies:
OperationRule
SELECTUser can view self, admins can view all
INSERTAdmins only
UPDATEUser can update self, admins can update all
DELETEAdmins only

super_admins

System-wide administrators with elevated access.
ColumnTypeDescription
idUUIDPrimary key
user_idUUIDReference to users
created_atTIMESTAMPTZWhen granted
created_byUUIDWho granted access
Indexes:
  • idx_super_admins_user_id on (user_id)
RLS Policies: Managed via service role only (not directly accessible via RLS)

push_tokens

Device tokens for push notifications.
ColumnTypeDescription
idUUIDPrimary key
auction_idUUIDParent auction
user_idUUIDDevice owner
tokenTEXTPush notification token
platformplatform_typeios or android
created_atTIMESTAMPTZRegistration timestamp
updated_atTIMESTAMPTZLast update
Indexes:
  • idx_push_tokens_token on (token)
  • idx_push_tokens_auction_id on (auction_id)
RLS Policies:
OperationRule
SELECTUser can view own tokens, admins can view all
INSERTUser can insert own tokens
UPDATEUser can update own tokens
DELETEUser can delete own, admins can delete all

Custom Enums

PostgreSQL enum types used throughout the schema:
EnumValuesUsed By
auction_roleowner, admin, team_memberauction_members
action_statuspending, completedproblem_actions
audit_actionuser_invited, user_joined, role_changed, user_removed, submission_created, inspection_created, problem_created, loginaudit_logs
channel_member_roleowner, memberchannel_members
kpi_typesafety, quality, delivery, cost, engagementproblem_kpi_impacts
message_typetext, image, systemmessages
platform_typeios, androidpush_tokens
problem_statusopen, in_progress, closedproblem_reports

Indexes Summary

The database uses 73 indexes optimized for common query patterns:

Index Types

TypeCountPurpose
B-tree65Standard lookups, sorting, range queries
GIN2Full-text search (messages), JSONB queries (report_data_rows)
Composite6Multi-column lookups for complex queries
Partial7Filtered indexes for common conditions (e.g., WHERE is_active = true)

Key Performance Indexes

-- Full-text search on chat messages
CREATE INDEX idx_messages_content_search
ON messages USING GIN (to_tsvector('english', content));

-- Efficient pagination for chat
CREATE INDEX idx_messages_channel_created
ON messages (channel_id, created_at DESC);

-- JSONB queries on report data
CREATE INDEX idx_report_data_rows_data
ON report_data_rows USING GIN (row_data);

-- Active datasets only
CREATE INDEX idx_report_datasets_status
ON report_datasets (status) WHERE status = 'active';

Views

Pre-built queries for common operations:

recent_submissions

Latest lot submissions with aggregated entries.
SELECT * FROM recent_submissions
WHERE auction_id = 'your-auction-id'
ORDER BY submitted_at DESC
LIMIT 50;
Columns: id, location_id, location_name, user_id, submitted_by, submitted_at, entries (JSON), entry_count, total_car_count, image_url, auction_id

recent_quality_inspections

Latest inspections with defect counts.
SELECT * FROM recent_quality_inspections
WHERE auction_id = 'your-auction-id'
  AND defect_count > 0;
Columns: id, location_id, location_name, user_id, inspector_name, vin_number, barcode, submitted_at, defect_count, auction_id

active_problem_reports

Open problems with action progress.
SELECT * FROM active_problem_reports
WHERE auction_id = 'your-auction-id'
  AND status != 'closed';
Columns: id, issue_name, area, location_id, location_name, status, prepared_by_id, prepared_by_name, assigned_to_id, assigned_to_name, action_count, completed_action_count, submitted_at, updated_at, auction_id

user_auctions

All auctions a user belongs to with branding.
SELECT * FROM user_auctions
WHERE user_id = auth.uid();
Columns: user_id, auction_id, auction_name, auction_slug, logo_url, primary_color, role, joined_at

auction_members_with_details

Members with full user information.
SELECT * FROM auction_members_with_details
WHERE auction_id = 'your-auction-id';
Columns: id, auction_id, user_id, email, full_name, role, joined_at, invited_by, invited_by_name

channel_unread_counts

Unread message counts per channel.
SELECT * FROM channel_unread_counts
WHERE user_id = auth.uid();
Columns: auction_id, channel_id, user_id, unread_count, last_message_at

messages_with_reply_count

Messages with thread statistics.
SELECT * FROM messages_with_reply_count
WHERE channel_id = 'channel-id'
  AND parent_message_id IS NULL
ORDER BY created_at DESC;
Columns: id, auction_id, channel_id, user_id, content, message_type, image_url, created_at, updated_at, parent_message_id, reply_count, last_reply_at, is_edited, is_deleted

RPC Functions

Authorization Functions

FunctionParametersReturnsDescription
is_super_admin()-BOOLEANCheck if current user is super admin
is_auction_member(p_auction_id)UUIDBOOLEANCheck auction membership
is_auction_admin(p_auction_id)UUIDBOOLEANCheck admin role
is_auction_admin_or_above(p_auction_id)UUIDBOOLEANCheck admin or owner
is_auction_owner(p_auction_id)UUIDBOOLEANCheck owner role
get_user_auction_role(p_auction_id)UUIDauction_roleGet user’s role
is_channel_member(p_channel_id)UUIDBOOLEANCheck channel membership
is_channel_owner(p_channel_id)UUIDBOOLEANCheck channel ownership
is_assigned_to_location(location_uuid)UUIDBOOLEANCheck location assignment

Auction Management

FunctionParametersReturnsDescription
invite_user_to_auction(...)auction_id, email, roleUUIDCreate invitation
accept_auction_invite(p_token)TEXTBOOLEANAccept and join
change_member_role(...)auction_id, user_id, new_roleBOOLEANUpdate member role
remove_auction_member(...)auction_id, user_idBOOLEANRemove member
transfer_auction_ownership(...)auction_id, new_owner_idBOOLEANTransfer ownership
get_invite_by_token(p_token)TEXTJSONGet invitation details

Submissions & Analytics

FunctionParametersReturnsDescription
create_lot_submission_with_entries(...)location_id, entries[], image_urlUUIDCreate multi-entry submission
get_submission_with_entries(p_submission_id)UUIDJSONGet submission with all entries
get_location_submission_types_ordered(p_location_id)UUIDSETOFGet ordered types for location
get_pending_submissions()-SETOFFind overdue submissions
get_location_stats(...)location_id, start_date, end_dateJSONLocation occupancy statistics
get_all_locations_stats(...)start_date, end_dateSETOFDashboard statistics
get_defect_trends(...)start_date, end_date, location_idSETOFQuality analytics

Chat Functions

FunctionParametersReturnsDescription
get_channel_notification_preference(p_channel_id)UUIDTEXTGet notification setting
mark_channel_as_read(p_channel_id)UUIDVOIDUpdate last_read_at
get_total_unread_count(p_auction_id)UUIDINTEGERCount all unread messages

Reporting Functions

FunctionParametersReturnsDescription
generate_share_token()-TEXTGenerate URL-safe token
create_report_share(...)report_id, share_type, user_id, expires_daysUUIDCreate report share
get_report_by_token(p_token)TEXTJSONGet public report
has_report_access(p_report_id)UUIDBOOLEANCheck report access
execute_report_query(...)dataset_id, sql_queryJSONSafe SQL execution with RLS

Storage Buckets

lot-images (Public)

Storage for submission and entry photos.
// Upload path structure
const path = `${userId}/${Date.now()}_${randomId}.jpg`;

// Upload example
const { data, error } = await supabase.storage
  .from('lot-images')
  .upload(path, file);
Policies:
  • Upload: Authenticated users can upload to their own folder
  • Read: Public access to all images
  • Delete: Users can delete own images; admins can delete any

avatars (Private)

Storage for user profile photos.
// Upload path structure
const path = `${userId}/${timestamp}.jpg`;

// Upload example
const { data, error } = await supabase.storage
  .from('avatars')
  .upload(path, compressedImage, {
    contentType: 'image/jpeg',
    upsert: true
  });

// Get public URL
const { data: { publicUrl } } = supabase.storage
  .from('avatars')
  .getPublicUrl(path);
Configuration:
  • File size limit: 5 MB
  • Allowed MIME types: image/jpeg, image/png, image/webp
  • Recommended dimensions: 256×256 pixels (mobile), 512×512 pixels (admin)
Policies:
OperationRule
UploadUsers can upload only to their own folder ({user_id}/*)
ReadUsers can read own avatar; admins can read any
UpdateUsers can replace own avatar only
DeleteUsers can delete own avatar only
When a user updates their avatar, the old file should be deleted to prevent storage bloat. The avatar URL is stored in the users.avatar_url column.

auction-branding (Public)

Storage for custom logos and branding assets.

Realtime Subscriptions

Tables with realtime enabled:
TableEventsUse Case
messagesINSERT, UPDATE, DELETELive chat delivery
message_reactionsINSERT, DELETEReal-time emoji reactions
channel_membersINSERT, DELETEMembership changes
// Subscribe to new messages
const channel = supabase
  .channel('messages')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'messages',
      filter: `channel_id=eq.${channelId}`
    },
    (payload) => handleNewMessage(payload.new)
  )
  .subscribe();

Entity Relationships


Migration Strategy

How Migrations Work

Supabase uses sequential SQL migration files to manage database schema changes. Migrations are version-controlled and applied in order based on timestamp prefixes.

Migration File Structure

supabase/migrations/
├── 20250101000000_initial_schema.sql           # Core tables
├── 20250101000001_seed_defect_categories.sql   # Seed data
├── 20250101000002_rls_policies.sql             # RLS policies
├── 20250101000003_functions_and_views.sql      # Functions/views
├── ...
└── 20250101000073_fix_token_lookup_padding.sql # Latest fix

Creating New Migrations

# Generate a new migration file
supabase migration new add_new_feature

# This creates:
# supabase/migrations/20250101000074_add_new_feature.sql

Migration Naming Conventions

PatternExampleUse Case
create_{table}_tablecreate_channels_table.sqlNew table
add_{column}_to_{table}add_auction_id_to_locations.sqlAdd column
create_{table}_rls_policiescreate_chat_rls_policies.sqlRLS policies
create_{name}_functioncreate_invite_user_function.sqlNew function
fix_{description}fix_chat_rls_infinite_recursion.sqlBug fixes
update_{table}_for_{feature}update_views_for_auction_context.sqlModify existing

Migration Best Practices

  1. Atomic Changes: Each migration should be self-contained and reversible
  2. Idempotent Scripts: Use IF NOT EXISTS and IF EXISTS clauses
  3. Test Locally: Run supabase db reset to test full migration chain
  4. RLS Tests: Update RLS tests when modifying policies
-- Example: Safe table creation
CREATE TABLE IF NOT EXISTS new_feature (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Example: Safe column addition
ALTER TABLE existing_table
ADD COLUMN IF NOT EXISTS new_column TEXT;

Running Migrations

# Apply pending migrations to local database
supabase db push

# Reset database and reapply all migrations
supabase db reset

# Check migration status
supabase migration list

Example Queries

Multi-Entry Lot Submission

// Create submission with multiple entry types
const { data, error } = await supabase.rpc('create_lot_submission_with_entries', {
  p_location_id: locationId,
  p_entries: [
    { submission_type_id: 'type-1-uuid', car_count: 25, image_url: 'https://...' },
    { submission_type_id: 'type-2-uuid', car_count: 12, image_url: 'https://...' }
  ],
  p_image_url: 'https://main-photo-url'
});
// Search messages with PostgreSQL full-text search
const { data, error } = await supabase
  .from('messages')
  .select('*')
  .textSearch('content', 'search terms')
  .eq('channel_id', channelId)
  .order('created_at', { ascending: false });

Report Data Query

// Execute AI-generated SQL safely
const { data, error } = await supabase.rpc('execute_report_query', {
  p_dataset_id: datasetId,
  p_sql_query: generatedSql
});

Next Steps