41 KiB
Database Direct Edit System
Overview
A full-featured database management interface for administrators (admin/superuser roles only) that allows direct CRUD operations on all database tables with advanced spreadsheet-like functionality, comprehensive filtering, sorting, and inline editing capabilities.
Status: 📋 Planned (Not Yet Implemented)
Target Users: Administrators and Superusers only
Security Level: Requires AAL2 (MFA verification)
Table of Contents
- Architecture & Security
- Core Components
- Feature Specifications
- Database Requirements
- Implementation Roadmap
- Dependencies
- Safety & UX Guidelines
Architecture & Security
Access Control
- Role Restriction: Only
adminandsuperuserroles can access - AAL2 Enforcement: All database operations require MFA verification via
useSuperuserGuard() - Audit Logging: Every modification logged to
admin_audit_log - Warning Banner: Display risk disclaimer about direct database access
- Read-Only Mode: Toggle to prevent accidental edits
Route Structure
/admin/database # Main database browser (table list)
/admin/database/:tableName # Spreadsheet editor for specific table
Navigation
- Add "Database Editor" link to AdminSidebar
- Icon:
Databasefrom lucide-react - Position: Below "User Management"
- Visibility: Superuser only (
isSuperuser())
Core Components
File Structure
src/
├── pages/admin/
│ └── AdminDatabase.tsx # Main page with routing
│
├── components/admin/database/
│ ├── index.ts # Barrel exports
│ ├── DatabaseTableBrowser.tsx # Table selector & overview
│ ├── DatabaseTableEditor.tsx # Main spreadsheet editor (TanStack Table)
│ ├── DatabaseTableFilters.tsx # Advanced filtering UI
│ ├── DatabaseColumnConfig.tsx # Column visibility/order management
│ ├── DatabaseRowEditor.tsx # Detailed row editor dialog
│ ├── DatabaseBulkActions.tsx # Bulk edit/delete operations
│ ├── DatabaseExportImport.tsx # CSV/JSON export/import
│ ├── DatabaseSchemaViewer.tsx # Table schema & ERD viewer
│ ├── DatabaseCellEditors.tsx # Type-specific cell editors
│ └── types.ts # TypeScript definitions
│
├── hooks/
│ ├── useTableSchema.ts # Fetch table schema from Supabase
│ ├── useTableData.ts # Fetch/edit table data with optimistic updates
│ ├── useDatabaseAudit.ts # Audit logging utilities
│ └── useDatabaseValidation.ts # Validation functions
│
└── lib/
├── database/
│ ├── cellEditors.tsx # Cell editor component factory
│ ├── filterFunctions.ts # Custom filter functions per data type
│ ├── validationRules.ts # Validation rules per column type
│ └── schemaParser.ts # Parse Supabase schema to table config
└── utils/
├── csvExport.ts # CSV export utilities
└── jsonImport.ts # JSON import/validation
Feature Specifications
Phase 1: Table Browser & Navigation
DatabaseTableBrowser Component
Purpose: Display all database tables with metadata and quick navigation
Features:
-
Table List Display:
- Grid or list view toggle
- Show table name, row count, size, last modified
- Search/filter tables by name
- Sort by name, row count, or date
-
Table Categorization:
const tableCategories = {
auth: {
color: 'red',
tables: ['profiles', 'user_roles', 'user_preferences', 'user_sessions'],
icon: 'Shield'
},
content: {
color: 'yellow',
tables: ['parks', 'rides', 'companies', 'ride_models', 'locations'],
icon: 'MapPin'
},
submissions: {
color: 'green',
tables: ['content_submissions', 'submission_items', 'photo_submissions'],
icon: 'FileText'
},
moderation: {
color: 'blue',
tables: ['reports', 'admin_audit_log', 'review_reports'],
icon: 'Flag'
},
versioning: {
color: 'purple',
tables: ['park_versions', 'ride_versions', 'company_versions'],
icon: 'History'
},
system: {
color: 'gray',
tables: ['admin_settings', 'notification_logs', 'rate_limits'],
icon: 'Settings'
}
}
-
Quick Stats Cards:
- Total tables count
- Total rows across all tables
- Database size
- Last modified timestamp
-
Table Actions:
- Click table to open editor
- Quick view schema (hover tooltip)
- Export table data
- View recent changes (from versions tables)
Data Fetching:
// Use Supabase RPC to get table metadata
const { data: tables } = await supabase.rpc('get_table_metadata')
interface TableMetadata {
table_name: string;
row_count: bigint;
total_size: string;
last_modified: string;
category?: string;
}
Phase 2: Spreadsheet-Style Table Editor
DatabaseTableEditor Component
Core Technology: TanStack Table v8 with advanced features
2.1 Data Grid Display
Features:
- Virtual Scrolling: Handle 10,000+ rows efficiently using
@tanstack/react-virtual - Sticky Headers: Column headers remain visible on scroll
- Row Numbers: Display row index in first column
- Column Resizing: Drag column borders to resize
- Column Reordering: Drag-drop column headers to reorder
- Row Selection:
- Single click to select row
- Shift+Click for range selection
- Ctrl+Click for multi-selection
- Checkbox column for bulk selection
- Zebra Striping: Alternate row colors for readability
- Cell Highlighting: Hover effect on cells
- Responsive Design: Horizontal scroll on smaller screens
Implementation:
const table = useReactTable({
data: tableData,
columns: dynamicColumns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
enableRowSelection: true,
enableMultiSort: true,
enableColumnResizing: true,
columnResizeMode: 'onChange',
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination
}
})
2.2 Inline Editing
Cell Editor Types (auto-detected from column type):
| Data Type | Editor Component | Features |
|---|---|---|
text, varchar |
<Input> |
Text input with validation |
integer, bigint, numeric |
<Input type="number"> |
Number input with min/max |
boolean |
<Switch> |
Toggle switch |
timestamp, date |
<DatePicker> |
Calendar popup with time |
uuid |
<Select> or <Input> |
FK lookup or manual entry |
jsonb, json |
<Textarea> + JSON validator |
Syntax highlighting |
enum |
<Select> |
Dropdown with allowed values |
text[] |
<TagInput> |
Multi-value chips |
Edit Flow:
- Click cell → Enter edit mode
- Show appropriate editor component
- Type/select new value
- Press Enter to save OR Escape to cancel
- Optimistic update → DB save → Rollback on error
- Visual feedback: Yellow (editing) → Green (saved) → Red (error)
Validation:
- Check NOT NULL constraints
- Validate data type compatibility
- Verify foreign key references exist
- Check max length for strings
- Validate JSON syntax
- Custom validation rules per table
Implementation:
const EditableCell = ({ getValue, row, column, table }) => {
const initialValue = getValue()
const [value, setValue] = useState(initialValue)
const [isEditing, setIsEditing] = useState(false)
const handleSave = async () => {
const meta = table.options.meta
await meta?.updateData(row.index, column.id, value)
setIsEditing(false)
}
return (
<div onClick={() => setIsEditing(true)}>
{isEditing ? (
<Input
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={handleSave}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSave()
if (e.key === 'Escape') setIsEditing(false)
}}
/>
) : (
<span>{value}</span>
)}
</div>
)
}
2.3 Advanced Filtering
Filter Panel (collapsible left sidebar):
Filter Builder UI:
- Add filter button
- Each filter row contains:
- Column selector dropdown
- Operator selector (based on column type)
- Value input (type-appropriate)
- Remove filter button
- AND/OR logic toggle between filters
- Clear all filters button
- Save filter preset (with name)
- Load saved preset
Filter Types by Data Type
Text Columns:
- Contains
- Equals
- Starts with
- Ends with
- Regex match
- Is empty / Is not empty
Number Columns:
- Equals
- Greater than (>)
- Less than (<)
- Greater than or equal (>=)
- Less than or equal (<=)
- Between (min, max)
- Is null / Is not null
Date Columns:
- Before
- After
- Between (start, end)
- Is null
- Last N days
- Next N days
Boolean Columns:
- Is true
- Is false
- Is null
UUID Columns:
- Equals
- In list (comma-separated)
- Is null
JSON Columns:
- Path exists
- Path equals value
- Contains key
Quick Filters Bar (above table):
- Chips showing active filters
- Click chip to edit filter
- X to remove filter
- "5 filters active" counter
Implementation:
interface ColumnFilter {
id: string; // column name
operator: FilterOperator;
value: any;
logicGate?: 'AND' | 'OR';
}
const filterFunctions = {
text: {
contains: (row, id, filterValue) =>
row.getValue(id)?.toLowerCase().includes(filterValue.toLowerCase()),
equals: (row, id, filterValue) =>
row.getValue(id) === filterValue,
startsWith: (row, id, filterValue) =>
row.getValue(id)?.startsWith(filterValue),
// ... more
},
number: {
equals: (row, id, filterValue) => row.getValue(id) === filterValue,
gt: (row, id, filterValue) => row.getValue(id) > filterValue,
// ... more
}
}
2.4 Sorting & Pagination
Sorting:
- Click column header to sort ascending
- Click again to sort descending
- Click third time to remove sort
- Shift+Click to add multi-column sort
- Sort indicators: ↑ (asc), ↓ (desc)
- Multi-sort shows order numbers (1, 2, 3)
- Custom sort functions for complex types (JSON, arrays)
Pagination:
- Rows per page selector: 25, 50, 100, 500, 1000
- Page navigation: First, Prev, Next, Last
- Page input: "Jump to page X"
- Display: "Showing 1-25 of 1,234 rows"
- "Load all" button (with warning for tables > 1000 rows)
- Virtual scrolling for large datasets
Implementation:
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 50
})
const table = useReactTable({
// ...
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
state: { pagination }
})
// Pagination controls
<div className="flex items-center gap-2">
<Button onClick={() => table.setPageIndex(0)}>First</Button>
<Button onClick={() => table.previousPage()}>Prev</Button>
<Input
type="number"
value={table.getState().pagination.pageIndex + 1}
onChange={(e) => table.setPageIndex(Number(e.target.value) - 1)}
/>
<span>of {table.getPageCount()}</span>
<Button onClick={() => table.nextPage()}>Next</Button>
<Button onClick={() => table.setPageIndex(table.getPageCount() - 1)}>Last</Button>
</div>
2.5 Column Management
Column Config Panel (collapsible right sidebar):
Features:
-
Column Visibility:
- Checkbox list of all columns
- Toggle individual columns on/off
- "Show all" / "Hide all" buttons
- Search column names
-
Column Reordering:
- Drag-drop list to reorder columns
- Visual drag handle icon
- Smooth reorder animation
-
Column Pinning:
- Pin columns to left (e.g., ID, name)
- Pin columns to right (e.g., actions)
- Unpin button
-
Column Presets:
- Save current column layout with name
- Load saved layouts
- Default layouts per table:
- "Essential" (ID, name, status, dates)
- "All" (show everything)
- "Metadata" (created_at, updated_at, created_by)
-
Reset to Default:
- Restore original column order/visibility
Implementation:
const [columnVisibility, setColumnVisibility] = useState({})
const [columnOrder, setColumnOrder] = useState([])
const [columnPinning, setColumnPinning] = useState({ left: ['id'], right: [] })
const table = useReactTable({
// ...
state: {
columnVisibility,
columnOrder,
columnPinning
},
onColumnVisibilityChange: setColumnVisibility,
onColumnOrderChange: setColumnOrder,
onColumnPinningChange: setColumnPinning
})
2.6 Search & Navigation
Global Search:
- Search input above table
- Searches all visible columns
- Highlight matching text in cells
- "X matches found" counter
- Next/Prev navigation between matches
- Clear search button
Quick Jump:
- "Jump to row by ID" input
- Autofocus and scroll to row
- Highlight target row
Keyboard Navigation:
- Arrow keys: Navigate cells
- Tab: Move to next editable cell
- Enter: Edit current cell / Save edit
- Escape: Cancel edit
- Page Up/Down: Scroll page
- Home/End: Jump to first/last row
- Ctrl+F: Focus search
- Ctrl+C: Copy cell value
- Del: Delete selected rows (with confirmation)
Phase 3: Advanced Features
3.1 Bulk Operations (DatabaseBulkActions)
Features:
-
Row Selection: Checkboxes in first column
-
Selection Actions:
- Select all on page
- Select all matching filter
- Deselect all
- Invert selection
-
Bulk Edit:
- Select column to edit
- Set value for all selected rows
- Preview changes before applying
- Apply button with confirmation
-
Bulk Delete:
- Delete all selected rows
- Show count: "Delete 23 rows?"
- Confirmation dialog with row preview
- Cannot be undone warning
-
Batch Update:
- Apply formula/function to column
- Examples:
- Set status to 'approved' for all pending
- Increment version number by 1
- Update timestamps to NOW()
-
Action History:
- Show last 10 bulk operations
- Display: operation, row count, timestamp, user
- Undo last operation (if safe and within 5 minutes)
UI:
<div className="bulk-actions-toolbar">
<span>{selectedRows.length} rows selected</span>
<Button onClick={handleBulkEdit}>Edit Selected</Button>
<Button onClick={handleBulkDelete} variant="destructive">Delete Selected</Button>
<Button onClick={handleDeselectAll} variant="ghost">Deselect All</Button>
</div>
3.2 Export/Import (DatabaseExportImport)
Export Features:
-
Export Formats:
- CSV (with headers)
- JSON (array of objects)
- SQL INSERT statements
-
Export Scope:
- All rows
- Filtered rows only
- Selected rows only
- Visible columns only
-
Export Options:
- Include/exclude NULL values
- Date format selection
- JSON pretty print
- SQL: Include CREATE TABLE statement
-
Batch Export:
- Export all tables as ZIP file
- Progress indicator for large exports
Import Features:
-
Import Formats:
- CSV (with column mapping UI)
- JSON (validate schema first)
-
Import Modes:
- Insert (add new rows)
- Update (match by ID)
- Upsert (insert or update)
-
Import Validation:
- Dry-run preview
- Show errors before import
- Skip invalid rows option
-
Import Progress:
- Progress bar for large files
- Success/error counts
- Download error log
Implementation:
const exportToCSV = (data: any[], columns: Column[]) => {
const headers = columns.map(col => col.header).join(',')
const rows = data.map(row =>
columns.map(col => JSON.stringify(row[col.id] ?? '')).join(',')
).join('\n')
const csv = `${headers}\n${rows}`
downloadFile(csv, 'table_export.csv', 'text/csv')
}
const importFromCSV = async (file: File) => {
const parsed = Papa.parse(file, { header: true })
// Validate schema
const validationErrors = validateImportData(parsed.data, tableSchema)
if (validationErrors.length > 0) {
showErrorDialog(validationErrors)
return
}
// Insert rows
for (const row of parsed.data) {
await supabase.from(tableName).insert(row)
}
}
3.3 Schema Viewer (DatabaseSchemaViewer)
Features:
-
Table Structure Display:
- Column list with details:
- Name
- Data type
- Nullable (✓/✗)
- Default value
- Max length (for text)
- Primary key indicator (🔑)
- Foreign key indicators (→ target_table)
- Unique constraint indicators
- Index indicators (📊)
- Column list with details:
-
Relationships View:
- Visual diagram (ERD style)
- Show foreign key relationships
- Click table to navigate
- Zoom/pan controls
-
Constraints:
- Primary keys
- Foreign keys (with ON DELETE/UPDATE actions)
- Unique constraints
- Check constraints
- Indexes (btree, gin, gist)
-
RLS Policies (read-only):
- List all policies for table
- Show policy name, operation, USING clause
- Syntax highlighting for SQL
-
SQL DDL View:
- Show CREATE TABLE statement
- Show CREATE INDEX statements
- Show RLS policies
- Copy to clipboard button
- Syntax highlighting
Implementation:
const SchemaViewer = ({ tableName }: { tableName: string }) => {
const { data: schema } = useTableSchema(tableName)
return (
<Tabs>
<TabsList>
<TabsTrigger value="columns">Columns</TabsTrigger>
<TabsTrigger value="relationships">Relationships</TabsTrigger>
<TabsTrigger value="constraints">Constraints</TabsTrigger>
<TabsTrigger value="rls">RLS Policies</TabsTrigger>
<TabsTrigger value="sql">SQL DDL</TabsTrigger>
</TabsList>
<TabsContent value="columns">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Nullable</TableHead>
<TableHead>Default</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{schema.columns.map(col => (
<TableRow key={col.name}>
<TableCell>
{col.isPrimaryKey && '🔑 '}
{col.name}
</TableCell>
<TableCell>{col.type}</TableCell>
<TableCell>{col.isNullable ? '✓' : '✗'}</TableCell>
<TableCell><code>{col.default}</code></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TabsContent>
{/* Other tabs */}
</Tabs>
)
}
3.4 Row Editor Dialog (DatabaseRowEditor)
Purpose: Detailed form-based editor for complex edits
Features:
-
Trigger: Double-click row to open
-
Layout: Vertical form with all columns
-
Field Types:
- Text:
<Textarea>for long text - JSON: Syntax-highlighted editor with validation
- Foreign Keys: Searchable
<Combobox>with preview - Images: Preview thumbnail + URL input
- Markdown: Split view (editor + preview)
- Arrays: Chip input for multi-value
- Timestamps: Calendar + time picker
- Text:
-
Actions:
- Save: Update row
- Save & Close: Update and return to table
- Clone Row: Duplicate with new ID
- Delete Row: With confirmation
- Cancel: Discard changes
-
Validation:
- Real-time field validation
- Show error messages inline
- Disable save until valid
-
Metadata Section:
- Show created_at, updated_at, created_by
- Show version history (if versioned table)
- Link to audit log entries
Implementation:
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit Row: {rowId}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSave}>
{columns.map(col => (
<div key={col.name} className="space-y-2">
<Label>{col.name}</Label>
{renderFieldEditor(col, formData[col.name], handleChange)}
{errors[col.name] && (
<p className="text-sm text-destructive">{errors[col.name]}</p>
)}
</div>
))}
<DialogFooter>
<Button variant="ghost" onClick={() => setIsOpen(false)}>Cancel</Button>
<Button variant="outline" onClick={handleClone}>Clone Row</Button>
<Button variant="destructive" onClick={handleDelete}>Delete</Button>
<Button type="submit">Save</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
3.5 Real-time Updates
Features:
-
Realtime Subscriptions:
- Subscribe to table changes via Supabase Realtime
- Listen for INSERT, UPDATE, DELETE events
- Show notification when another admin edits
-
Conflict Detection:
- Detect concurrent edits on same row
- Show warning: "User X is editing this row"
- Lock row to prevent conflicts (optional)
-
Auto-refresh:
- Toggle auto-refresh on/off
- Refresh interval: 10s, 30s, 60s
- Manual refresh button
-
Change Notifications:
- Toast notification: "Table updated by Admin (3 rows changed)"
- Click to refresh table
- Dismiss notification
Implementation:
useEffect(() => {
const channel = supabase
.channel(`table:${tableName}`)
.on('postgres_changes',
{
event: '*',
schema: 'public',
table: tableName
},
(payload) => {
toast.info(`Table updated: ${payload.eventType}`)
refetch() // Refetch table data
}
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [tableName])
3.6 Audit Trail Integration
Features:
-
Automatic Logging:
- Every edit logged to
admin_audit_log - Log: admin_user_id, table, row_id, action, old_values, new_values, timestamp
- Every edit logged to
-
Recent Changes Panel (sidebar):
- Show last 20 changes on current table
- Display: timestamp, user, action, affected columns
- Click to view full details
-
Change Details Dialog:
- Show before/after diff
- Highlight changed fields
- Show user who made change
- Link to row in table
-
Revert Functionality:
- Revert single change (if safe)
- Check for dependent changes
- Confirmation dialog
- Cannot revert if related data deleted
Implementation:
const logDatabaseEdit = async (
tableName: string,
rowId: string,
operation: 'INSERT' | 'UPDATE' | 'DELETE',
oldValues: any,
newValues: any
) => {
await supabase.rpc('log_database_direct_edit', {
_table_name: tableName,
_row_id: rowId,
_operation: operation,
_old_values: oldValues,
_new_values: newValues
})
}
// In updateData function:
const handleUpdate = async (rowIndex, columnId, value) => {
const row = tableData[rowIndex]
const oldValue = row[columnId]
// Update database
await supabase.from(tableName).update({ [columnId]: value }).eq('id', row.id)
// Log audit trail
await logDatabaseEdit(
tableName,
row.id,
'UPDATE',
{ [columnId]: oldValue },
{ [columnId]: value }
)
}
Database Requirements
Supabase RPC Functions
1. get_table_metadata()
Returns metadata for all tables in public schema.
CREATE OR REPLACE FUNCTION get_table_metadata()
RETURNS TABLE (
table_name text,
row_count bigint,
total_size text,
last_modified timestamp with time zone
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = 'public'
AS $$
BEGIN
RETURN QUERY
SELECT
t.tablename::text as table_name,
COALESCE(c.reltuples::bigint, 0) as row_count,
pg_size_pretty(pg_total_relation_size(t.schemaname||'.'||t.tablename)) as total_size,
COALESCE(
(SELECT MAX(created_at) FROM information_schema.tables WHERE table_name = t.tablename),
NOW()
) as last_modified
FROM pg_tables t
LEFT JOIN pg_class c ON c.relname = t.tablename
WHERE t.schemaname = 'public'
ORDER BY t.tablename;
END;
$$;
-- Grant execute to admins only
REVOKE EXECUTE ON FUNCTION get_table_metadata() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION get_table_metadata() TO authenticated;
-- RLS policy: only admins/superusers can execute
CREATE POLICY "admin_only_table_metadata" ON ... -- (handled in calling code)
2. get_table_schema(table_name text)
Returns detailed schema information for a specific table.
CREATE OR REPLACE FUNCTION get_table_schema(_table_name text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = 'public'
AS $$
DECLARE
schema_info jsonb;
BEGIN
-- Get column information
SELECT jsonb_agg(
jsonb_build_object(
'name', column_name,
'type', data_type,
'isNullable', is_nullable = 'YES',
'maxLength', character_maximum_length,
'numericPrecision', numeric_precision,
'defaultValue', column_default,
'isPrimaryKey', EXISTS (
SELECT 1 FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_name = _table_name
AND tc.constraint_type = 'PRIMARY KEY'
AND kcu.column_name = c.column_name
),
'foreignKey', (
SELECT jsonb_build_object(
'table', ccu.table_name,
'column', ccu.column_name
)
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.table_name = _table_name
AND tc.constraint_type = 'FOREIGN KEY'
AND kcu.column_name = c.column_name
LIMIT 1
),
'enumValues', (
SELECT array_agg(enumlabel)
FROM pg_enum e
JOIN pg_type t ON t.oid = e.enumtypid
WHERE t.typname = c.udt_name
)
)
)
INTO schema_info
FROM information_schema.columns c
WHERE table_name = _table_name
AND table_schema = 'public';
RETURN schema_info;
END;
$$;
3. log_database_direct_edit(...)
Logs all direct database edits to audit trail.
CREATE OR REPLACE FUNCTION log_database_direct_edit(
_table_name text,
_row_id uuid,
_operation text,
_old_values jsonb,
_new_values jsonb
)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = 'public'
AS $$
BEGIN
INSERT INTO admin_audit_log (
admin_user_id,
target_user_id,
action,
details
) VALUES (
auth.uid(),
NULL, -- No specific target user for direct DB edits
'direct_database_edit',
jsonb_build_object(
'table', _table_name,
'row_id', _row_id,
'operation', _operation,
'old_values', _old_values,
'new_values', _new_values,
'timestamp', NOW()
)
);
END;
$$;
RLS Policies
All tables must enforce AAL2 for admin/superuser direct edits:
-- Example policy for parks table
CREATE POLICY "admin_direct_edit_requires_aal2"
ON parks
FOR ALL
USING (
(
EXISTS (
SELECT 1 FROM user_roles
WHERE user_id = auth.uid()
AND role IN ('admin', 'superuser')
)
)
AND (
(auth.jwt() ->> 'aal') = 'aal2'
OR NOT EXISTS (
SELECT 1 FROM auth.mfa_factors
WHERE user_id = auth.uid()
AND status = 'verified'
)
)
);
-- Apply to all tables:
-- parks, rides, companies, ride_models, locations, profiles, etc.
Implementation Roadmap
Sprint 1: Foundation (Days 1-3)
Goal: Set up routes, navigation, and basic table browser
Tasks:
- ✅ Create
/admin/databaseroute in router - ✅ Create
AdminDatabase.tsxpage component - ✅ Add "Database Editor" link to AdminSidebar (superuser only)
- ✅ Implement access control:
- Check
isAdmin()orisSuperuser() - Enforce AAL2 with
useSuperuserGuard() - Redirect if unauthorized
- Check
- ✅ Create
DatabaseTableBrowser.tsxcomponent - ✅ Implement
get_table_metadata()RPC function - ✅ Fetch and display table list with metadata
- ✅ Add table categorization and color coding
- ✅ Implement table search/filter
- ✅ Add warning banner about direct DB access
Deliverables:
- Working
/admin/databasepage - Table list with categories
- Navigation from sidebar
- AAL2 enforcement
Sprint 2: Core Editor (Days 4-7)
Goal: Build main spreadsheet editor with basic editing
Tasks:
- ✅ Install dependencies:
@tanstack/react-virtual - ✅ Create
/admin/database/:tableNameroute - ✅ Create
DatabaseTableEditor.tsxcomponent - ✅ Implement
get_table_schema()RPC function - ✅ Fetch table schema and generate dynamic columns
- ✅ Set up TanStack Table with:
- Core row model
- Pagination
- Row selection
- ✅ Implement basic inline editing:
- Text fields
- Number fields
- Boolean switches
- ✅ Add optimistic updates with rollback
- ✅ Implement save/error handling
- ✅ Add visual feedback (yellow → green → red)
- ✅ Create
log_database_direct_edit()RPC function - ✅ Integrate audit logging on all edits
Deliverables:
- Working table editor page
- Inline editing for basic types
- Audit logging functional
Sprint 3: Advanced Editing (Days 8-10)
Goal: Complete all cell editors, filtering, sorting, column config
Tasks:
- ✅ Implement all cell editor types:
- Date/Timestamp picker
- UUID foreign key selector
- JSON editor with validation
- Enum dropdown
- Array/tag input
- ✅ Create
DatabaseTableFilters.tsxcomponent - ✅ Implement filter builder UI
- ✅ Add filter functions for all data types
- ✅ Integrate TanStack Table filtering
- ✅ Add quick filters bar
- ✅ Implement saved filter presets
- ✅ Add multi-column sorting (Shift+Click)
- ✅ Create
DatabaseColumnConfig.tsxcomponent - ✅ Implement column visibility toggle
- ✅ Add column reordering (drag-drop)
- ✅ Implement column pinning
- ✅ Add column layout presets
- ✅ Implement keyboard navigation
- ✅ Add global search
Deliverables:
- All cell editors working
- Advanced filtering UI
- Multi-column sorting
- Column management panel
- Keyboard shortcuts
Sprint 4: Bulk & Export (Days 11-12)
Goal: Add bulk operations and import/export functionality
Tasks:
- ✅ Install dependencies:
papaparse - ✅ Create
DatabaseBulkActions.tsxcomponent - ✅ Implement bulk edit functionality
- ✅ Implement bulk delete with confirmation
- ✅ Add batch update feature
- ✅ Create action history with undo
- ✅ Create
DatabaseExportImport.tsxcomponent - ✅ Implement CSV export
- ✅ Implement JSON export
- ✅ Implement SQL export
- ✅ Add export options (scope, format)
- ✅ Implement CSV import with validation
- ✅ Implement JSON import
- ✅ Add import preview/dry-run
- ✅ Create batch export (all tables as ZIP)
Deliverables:
- Bulk operations working
- Export in multiple formats
- Import with validation
- Action history with undo
Sprint 5: Polish & Safety (Days 13-14)
Goal: Add schema viewer, safety features, and final polish
Tasks:
- ✅ Create
DatabaseSchemaViewer.tsxcomponent - ✅ Implement columns view
- ✅ Add relationships/ERD view
- ✅ Display constraints and indexes
- ✅ Show RLS policies (read-only)
- ✅ Add SQL DDL view with syntax highlighting
- ✅ Implement read-only mode toggle
- ✅ Add confirmation dialogs for all destructive actions
- ✅ Implement real-time updates (Supabase Realtime)
- ✅ Create recent changes panel
- ✅ Add change details dialog with diff view
- ✅ Implement revert functionality (if safe)
- ✅ Add all safety warnings and checks
- ✅ Full testing (manual + automated if time)
- ✅ Write documentation (this file)
Deliverables:
- Complete schema viewer
- All safety features
- Real-time updates
- Full testing
- Documentation
Dependencies
Required npm Packages
# Virtual scrolling for large tables
npm install @tanstack/react-virtual
# JSON viewer/editor
npm install react-json-view-lite
# Syntax highlighting
npm install prismjs
npm install @types/prismjs --save-dev
# CSV parsing/generation
npm install papaparse
npm install @types/papaparse --save-dev
Already Installed (✅)
@tanstack/react-table- Table managementlucide-react- Iconsreact-hook-form- Form handlingzod- Validationdate-fns- Date utilitiessonner- Toast notifications
Safety & UX Guidelines
Safety Features Checklist
Confirmation Dialogs
- Delete single row → "Delete this row?"
- Delete multiple rows → "Delete X rows? This cannot be undone."
- Bulk edit → "Update X rows?"
- Drop table → "⚠️ DANGER: Delete entire table?"
- Import data → "Import X rows? Existing data may be overwritten."
Validation
- Enforce NOT NULL constraints
- Check data type compatibility
- Validate foreign key references exist
- Check max length for strings
- Validate JSON syntax
- Prevent orphan records (check dependencies)
Access Control
- AAL2 enforcement on page load
- Re-check AAL2 before each destructive operation
- Show MFA prompt if session drops to AAL1
- Log all operations to audit trail
Error Handling
- Rollback optimistic updates on error
- Show clear error messages
- Prevent partial updates (use transactions)
- Handle foreign key constraint violations gracefully
Data Integrity
- Warn before editing critical tables (auth, profiles)
- Prevent editing system columns (id, created_at, etc.)
- Check for circular foreign key dependencies
- Validate enum values against allowed list
UX Best Practices
Loading States
// Skeleton loader for table
{isLoading && <TableSkeleton rows={10} cols={8} />}
// Cell-level loading (during save)
{isSaving && <Spinner className="w-4 h-4" />}
// Progress bar for bulk operations
<Progress value={progress} max={total} />
Error Feedback
// Toast notifications
toast.error('Failed to save: Foreign key violation')
toast.success('Successfully updated 5 rows')
toast.warning('Table has unsaved changes')
// Inline validation
<Input
error={errors.email?.message}
className={errors.email && 'border-destructive'}
/>
Keyboard Shortcuts
| Shortcut | Action |
|---|---|
Ctrl+F |
Focus search |
Ctrl+S |
Save current row |
Ctrl+Z |
Undo last change |
Del |
Delete selected rows |
Ctrl+E |
Toggle edit mode |
Escape |
Cancel edit / Close dialog |
Enter |
Save edit / Submit form |
Arrow keys |
Navigate cells |
Tab |
Move to next cell |
Shift+Tab |
Move to previous cell |
Page Up/Down |
Scroll page |
Home/End |
Jump to first/last row |
? |
Show keyboard shortcuts help |
Responsive Design
- Horizontal scroll for tables on mobile
- Collapsible filter/column panels
- Touch-friendly cell editors (larger tap targets)
- Stack filters vertically on small screens
- Hide less important columns by default on mobile
Dark Mode Support
/* Table grid */
.table-cell {
@apply border-border/40;
@apply hover:bg-accent/50;
}
/* Cell states */
.cell-editing { @apply bg-yellow-500/20 dark:bg-yellow-500/10; }
.cell-saved { @apply bg-green-500/20 dark:bg-green-500/10; }
.cell-error { @apply bg-red-500/20 dark:bg-red-500/10; }
/* Syntax highlighting */
.json-editor {
@apply bg-background text-foreground;
}
Security Considerations
Authentication & Authorization
- AAL2 Enforcement: All database operations require MFA verification
- Role-Based Access: Only admin/superuser roles can access
- Session Monitoring: Detect if session drops to AAL1 during use
- Token Refresh: Handle token expiration gracefully
Data Protection
- Audit Logging: Every edit logged with user, timestamp, before/after values
- RLS Policies: Enforce row-level security even in admin UI
- Input Sanitization: Validate all user input before DB writes
- SQL Injection Prevention: Use parameterized queries (Supabase handles this)
Rate Limiting
-- Prevent abuse: max 1000 edits per admin per hour
CREATE POLICY "rate_limit_admin_edits"
ON admin_audit_log
FOR INSERT
WITH CHECK (
(SELECT COUNT(*) FROM admin_audit_log
WHERE admin_user_id = auth.uid()
AND created_at > NOW() - INTERVAL '1 hour'
AND action = 'direct_database_edit') < 1000
);
Dangerous Operations Warning
Display prominent warnings for:
- Editing auth-related tables (profiles, user_roles)
- Deleting > 100 rows at once
- Importing > 1000 rows
- Editing production environment (if detected)
Testing Strategy
Manual Testing Checklist
- AAL2 enforcement blocks AAL1 users
- Non-admin users cannot access page
- All table categories display correctly
- Table search/filter works
- Table editor loads data correctly
- Inline editing saves successfully
- Optimistic updates rollback on error
- All cell editor types work
- Filtering works for all data types
- Multi-column sorting works
- Column visibility toggle works
- Column reordering works
- Pagination works correctly
- Global search highlights matches
- Keyboard navigation works
- Bulk edit updates multiple rows
- Bulk delete confirms and deletes
- Export CSV/JSON/SQL works
- Import validates and inserts data
- Schema viewer displays correctly
- Real-time updates show notifications
- Audit trail logs all edits
- Recent changes panel displays edits
- Revert functionality works
- Confirmation dialogs show for destructive actions
- Read-only mode prevents edits
- Dark mode styling looks good
Automated Testing (Future)
// Example Playwright test
test('admin can edit table cell', async ({ page }) => {
await loginAsAdmin(page)
await page.goto('/admin/database/parks')
// Click cell to edit
await page.click('[data-cell="name-0"]')
await page.fill('input', 'New Park Name')
await page.press('input', 'Enter')
// Wait for save
await page.waitForSelector('.cell-saved')
// Verify in database
const park = await getParkByName('New Park Name')
expect(park).toBeTruthy()
})
Future Enhancements
Phase 2 Features (Post-MVP)
-
SQL Query Console:
- Raw SQL query interface
- Query history
- Result set export
- Query templates/snippets
-
Visual Query Builder:
- Drag-drop JOIN builder
- Visual WHERE clause builder
- Preview results
-
Scheduled Jobs:
- Schedule bulk updates
- Automated cleanup scripts
- Email reports
-
Advanced Analytics:
- Table usage statistics
- Most edited tables
- Edit frequency heatmap
- Admin activity dashboard
-
Version Control Integration:
- Git-like diff view
- Branch/merge for schema changes
- Rollback to previous states
-
Collaboration Features:
- Live presence indicators (who's viewing)
- Comments on rows
- Change proposals (before applying)
- Team chat in context
Troubleshooting
Common Issues
Issue: "AAL2 required" error blocks access
- Solution: Ensure user has MFA enrolled and verified. Prompt to verify MFA if session is at AAL1.
Issue: Table loads slowly (> 5s)
- Solution: Add pagination, reduce pageSize, add indexes to frequently filtered columns.
Issue: Foreign key dropdown shows too many options
- Solution: Add search/autocomplete to dropdown, paginate results, show only recent 100.
Issue: JSON editor loses formatting on save
- Solution: Ensure JSON is pretty-printed before display, preserve whitespace.
Issue: Real-time updates cause table to jump
- Solution: Use optimistic updates, throttle refresh notifications, show "refresh available" button instead of auto-refresh.
Issue: Export fails for large tables (> 10k rows)
- Solution: Stream export instead of loading all in memory, show progress bar, split into chunks.
Related Documentation
- DATABASE_ARCHITECTURE.md - Database schema and RLS policies
- ARCHITECTURE_OVERVIEW.md - System architecture
- ADMIN_FEATURES.md - Other admin features (if exists)
Change Log
| Date | Version | Changes | Author |
|---|---|---|---|
| 2025-01-XX | 1.0.0 | Initial documentation | System |
Approval & Sign-off
Status: 📋 Planned (awaiting approval)
Reviewed By: Pending
Approved By: Pending
Implementation Start Date: TBD
Expected Completion: TBD (14 days after start)
End of Document