Admin UI
Internal admin app for managing workspaces, MLS connections, and sync operations
Admin UI Specification
Internal admin app for managing workspaces, MLS connections, and sync operations.
Overview
The admin app is an internal tool for:
- Onboarding new clients (workspaces)
- Configuring MLS connections
- Managing field mappings
- Monitoring sync operations
- Configuring compliance text
Access: Internal team only (authenticated via Supabase Auth, role = admin)
Route Structure
/admin
├── /login # Auth
├── /workspaces # List all workspaces
├── /workspaces/new # Create workspace
└── /workspaces/[id] # Workspace detail
├── /overview # Dashboard (default)
├── /settings # Site settings (agent, brokerage, theme)
├── /service-areas # Service area management
├── /imports # Import task management
├── /photos # Photo management
├── /mls # MLS connection config
├── /mapping # Field mapping editor
├── /search # Search config (facets, sorts)
├── /compliance # Attribution/disclaimer
└── /sync # Sync monitorScreens
1. Login (/admin/login)
Simple auth screen using Supabase Auth.
Components:
- Email/password form
- "Sign in" button
- Error display
Flow:
- User enters credentials
- Call
supabase.auth.signInWithPassword() - On success, check
user_profiles.role === 'admin' - Redirect to
/admin/workspaces
2. Workspace List (/admin/workspaces)
Grid/table of all workspaces.
Columns:
| Column | Description |
|---|---|
| Name | Workspace name (link to detail) |
| Slug | URL slug |
| Status | active/paused/archived (badge) |
| Domains | Primary domain |
| Listings | Count of active listings |
| Last Sync | Timestamp + status indicator |
| Actions | Edit, Pause, Archive |
Actions:
- "New Workspace" button →
/admin/workspaces/new - Row click →
/admin/workspaces/[id]/overview - Quick status toggle (active ↔ paused)
Data Loading:
const workspaces = await supabase
.from('workspaces')
.select(`
*,
workspace_domains(domain, is_primary),
mls_connections(status, last_synced_at),
listings(count)
`)
.order('created_at', { ascending: false });3. Create Workspace (/admin/workspaces/new)
Form to create new workspace.
Fields:
| Field | Type | Required | Notes |
|---|---|---|---|
| Name | text | Yes | Display name |
| Slug | text | Yes | Auto-generated from name, editable |
| Primary Domain | text | No | e.g., laineylevin.com |
On Submit:
- Create
workspacesrow - Create
workspace_domainsrow (if domain provided) - Create
compliance_profilesrow with defaults - Redirect to
/admin/workspaces/[id]/mls
4. Site Settings (/admin/workspaces/[id]/settings)
Configure workspace branding and site customization.
Sections:
Agent Profile
- Name, title, email, phone
- Photo upload
- License number
- Bio (Markdown supported)
Brokerage Info
- Brokerage name
- Logo upload
- Address, phone, website
Social Links
- Instagram, Facebook, LinkedIn
- Twitter, YouTube, TikTok
Site Customization
- Hero image URL
- Hero tagline
- Featured listing selector
- "Show All Listings" toggle
Theme
- Preset selector (luxury, modern, coastal, classic)
- Color overrides (optional)
Form with Change Detection: The settings form tracks changes and prompts before navigating away with unsaved edits.
4a. Service Areas (/admin/workspaces/[id]/service-areas)
Manage geographic service areas.
Service Area List:
| Column | Description |
|---|---|
| Name | Area name (link to edit) |
| Slug | URL slug |
| Listings | Count of matching listings |
| Import Status | Has linked import task |
| Order | Display order |
| Actions | Edit, Delete, Create Import |
Create/Edit Form:
- Name, slug, tagline
- Description (rich text)
- Hero image URL
- Filter configuration:
- Cities (multi-select)
- Subdivisions (multi-select)
- Postal codes
- Building names
- Price range
- Property types
Actions:
- "Create Import Task" → Creates import task from area filter
- Reorder via drag-and-drop
4b. Import Tasks (/admin/workspaces/[id]/imports)
Manage listing import tasks.
Import Task List:
| Column | Description |
|---|---|
| Name | Task name (link to edit) |
| Category | personal/office/search/custom (badge) |
| Status | Enabled/Disabled |
| Listings | Count of imported listings |
| Last Sync | Timestamp + status |
| Actions | Run, Toggle, Edit, Delete |
Create/Edit Form:
- Name, description
- Category selector
- Filter configuration (varies by category)
- Status filters (Active, Pending, etc.)
- Price range
- Max import limit
- Sync cadence (manual/hourly/daily)
- Visibility toggles
Actions:
- "Run Sync" → Triggers immediate sync
- Enable/Disable toggle
- View sync history
4c. Photo Management (/admin/workspaces/[id]/photos)
Manage photos for workspace listings.
Listing Selector:
- Search/filter listings
- Show photo count per listing
Photo Grid (for selected listing):
- Drag-and-drop reordering
- Primary photo indicator
- Upload new photos
- Delete photos
- Set primary photo
Bulk Actions:
- Upload multiple photos
- Clear all photos
5. Workspace Overview (/admin/workspaces/[id]/overview)
Dashboard view of workspace health.
Sections:
Status Card
- Workspace status (active/paused/archived)
- Quick actions: Pause/Resume, Archive
Stats Cards (grid)
- Total listings
- Active listings
- Pending listings
- Closed listings (last 30 days)
Domains
- List of configured domains
- Primary indicator
- Add domain button
Last Sync
- Timestamp
- Status (success/error)
- Stats (fetched, upserted, indexed)
- Link to sync monitor
Quick Links
- "Configure MLS" →
/mls - "Edit Mapping" →
/mapping - "View Sync Logs" →
/sync - "Run Sync Now" → trigger sync
5. MLS Connection (/admin/workspaces/[id]/mls)
Configure Bridge API connection.
Form Fields:
| Field | Type | Notes |
|---|---|---|
| Provider | select | bridge (readonly for now) |
| Base URL | text | Default: https://api.bridgedataoutput.com/api/v2/OData |
| Dataset ID | text | e.g., test or production dataset |
| Access Token | password | Encrypted on save |
| Sync Cadence | select | manual / hourly / daily |
Actions:
- "Test Connection" button
- Calls
testConnection(workspaceId) - Shows success/error toast
- On success, shows sample listing count
- Calls
- "Save" button
- Upserts
mls_connectionsrow - Encrypts token before storing
- Upserts
Test Connection UI:
┌─────────────────────────────────────┐
│ ✓ Connection successful! │
│ Found 2,847 listings in dataset │
└─────────────────────────────────────┘6. Field Mapping (/admin/workspaces/[id]/mapping)
Edit RESO → canonical field mappings.
UI Layout:
- Left: JSON editor (Monaco or CodeMirror)
- Right: Field reference + validation status
Features:
- Load current active mapping
- Edit JSON
- "Validate" button
- Calls
validateMapping(workspaceId) - Fetches 5 sample listings
- Shows which fields mapped successfully
- Shows missing/unmapped fields
- Calls
- "Save as New Version" button
- Creates new
field_mappingsrow withversion++ - Sets
is_active = true, deactivates previous
- Creates new
Validation Display:
✓ source_listing_key: ListingKey → "12345ABC"
✓ standard_status: StandardStatus → "Active"
✓ city: City → "Miami Beach"
✗ building_name: BuildingName → null (field not in source)
⚠ lot_size_sqft: LotSizeSquareFeet → undefined (check field name)Version History:
- Table of previous mapping versions
- Created date
- "Restore" action (creates new version from old)
7. Search Config (/admin/workspaces/[id]/search)
Configure Typesense search behavior.
Facet Toggles: Checkboxes for each available facet:
- City
- Subdivision
- Building Name
- Property Type
- Status
- Bedrooms
- Bathrooms
- Price Range
Sort Options: Drag-and-drop reorder:
- Newest First
- Price: Low to High
- Price: High to Low
- Bedrooms
- Square Footage
Default Sort: Dropdown to select default
Save: Updates workspace config (could be JSON in workspaces table or separate table)
8. Compliance (/admin/workspaces/[id]/compliance)
MLS attribution and disclaimer configuration.
Fields:
| Field | Type | Notes |
|---|---|---|
| Attribution HTML | textarea/rich text | Required MLS text |
| Disclaimer HTML | textarea/rich text | Legal disclaimer |
| Show on Search | checkbox | Display on search results |
| Show on Detail | checkbox | Display on listing detail |
Preview: Live preview of rendered HTML
Save: Upserts compliance_profiles row
Default Attribution Example:
<p>Listing information provided by Miami Realtors®.
Information deemed reliable but not guaranteed.</p>9. Sync Monitor (/admin/workspaces/[id]/sync)
View sync history and trigger manual syncs.
Actions:
- "Run Full Sync" button (with confirmation)
- "Run Incremental Sync" button
Sync Runs Table:
| Column | Description |
|---|---|
| Started | Timestamp |
| Duration | Time to complete |
| Status | running/success/error (badge) |
| Fetched | Listings fetched from API |
| Upserted | Listings written to DB |
| Indexed | Documents indexed to Typesense |
| Errors | Error count |
| Actions | View details |
Filters:
- Status filter (all/success/error)
- Date range
Sync Detail Modal:
- Full stats JSON
- Error message (if failed)
- Timestamp details
Live Status (when sync running):
┌─────────────────────────────────────┐
│ 🔄 Sync in progress... │
│ Fetched: 847 / ~2,800 │
│ Elapsed: 2m 34s │
└─────────────────────────────────────┘Components
Shared Components
/components/admin
├── AdminLayout.tsx # Sidebar + header wrapper
├── WorkspaceSidebar.tsx # Workspace-specific nav
├── StatusBadge.tsx # Status indicator (active/paused/error)
├── StatCard.tsx # Metric display card
├── DataTable.tsx # Reusable table with sorting/filtering
├── JsonEditor.tsx # Monaco-based JSON editor
├── ConfirmDialog.tsx # Confirmation modal
└── Toast.tsx # Toast notificationsAdmin Layout
┌─────────────────────────────────────────────────────────┐
│ 1 OAK Admin [User] [Logout] │
├──────────────┬──────────────────────────────────────────┤
│ │ │
│ Workspaces │ │
│ ─────────── │ [Content Area] │
│ > Lainey │ │
│ Client 2 │ │
│ │ │
│ ─────────── │ │
│ + New │ │
│ │ │
└──────────────┴──────────────────────────────────────────┘Workspace Sidebar (when viewing workspace)
┌──────────────┐
│ ← Back │
│ │
│ Lainey Levin │
│ ═══════════ │
│ │
│ Overview │
│ Settings │
│ Service Areas│
│ Imports │
│ Photos │
│ ─────────── │
│ MLS │
│ Mapping │
│ Search │
│ Compliance │
│ Sync │
│ │
└──────────────┘API Routes
// Workspace CRUD
POST /api/admin/workspaces
GET /api/admin/workspaces
GET /api/admin/workspaces/[id]
PATCH /api/admin/workspaces/[id]
DELETE /api/admin/workspaces/[id]
// MLS Connection
GET /api/admin/workspaces/[id]/mls
PUT /api/admin/workspaces/[id]/mls
POST /api/admin/workspaces/[id]/mls/test
// Import Tasks
GET /api/admin/workspaces/[id]/imports
POST /api/admin/workspaces/[id]/imports
PATCH /api/admin/workspaces/[id]/imports/[taskId]
DELETE /api/admin/workspaces/[id]/imports/[taskId]
POST /api/admin/workspaces/[id]/imports/[taskId]/toggle
POST /api/admin/workspaces/[id]/imports/[taskId]/run
// Field Mapping
GET /api/admin/workspaces/[id]/mapping
POST /api/admin/workspaces/[id]/mapping
POST /api/admin/workspaces/[id]/mapping/validate
// Compliance
GET /api/admin/workspaces/[id]/compliance
PUT /api/admin/workspaces/[id]/compliance
// Sync
GET /api/admin/workspaces/[id]/sync/runs
POST /api/admin/workspaces/[id]/sync/run
GET /api/admin/workspaces/[id]/sync/runs/[runId]Dashboard API (Agent-facing)
// Service Areas
GET /api/dashboard/service-areas
POST /api/dashboard/service-areas
PUT /api/dashboard/service-areas/[id]
DELETE /api/dashboard/service-areas/[id]
GET /api/dashboard/service-areas/import-status
// Imports
GET /api/dashboard/imports
POST /api/dashboard/imports/[id]/sync
POST /api/dashboard/imports/from-service-areas
// Photos
GET /api/dashboard/photos?listing_id=xxx
POST /api/dashboard/photos
DELETE /api/dashboard/photos/[id]
PATCH /api/dashboard/photos/[id]
POST /api/dashboard/photos/reorderAuthentication & Authorization
Middleware
// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
export async function middleware(request: NextRequest) {
const res = NextResponse.next();
const supabase = createMiddlewareClient({ req: request, res });
const { data: { session } } = await supabase.auth.getSession();
// Protect /admin routes
if (request.nextUrl.pathname.startsWith('/admin')) {
if (!session) {
return NextResponse.redirect(new URL('/admin/login', request.url));
}
// Check admin role
const { data: profile } = await supabase
.from('user_profiles')
.select('role')
.eq('id', session.user.id)
.single();
if (profile?.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
}
return res;
}