Architecture Overview
Executive Summary
This document provides a comprehensive overview of the Nounspace configuration architecture. The system has been refactored from a static, build-time TypeScript-based configuration system to a dynamic, database-backed, multi-tenant runtime configuration system that supports domain-based community detection.
Core Architectural Principles
- Server-Only Config Loading:
loadSystemConfig()is server-only and usesawait headers()API - Prop-Based Config Passing: Client components receive config via
systemConfigprop from Server Components - Runtime Configuration Loading: All community configs are loaded from Supabase at request time
- Multi-Tenant Support: Single deployment serves multiple communities via domain-based routing
- Separation of Concerns: Configs, themes, and pages are stored in different locations
- Dynamic Navigation: Navigation pages are stored as Spaces in Supabase Storage
- Simplified Space Creators: All communities use Nouns implementations for initial space creation
Architecture Layers
1. Request Flow & Domain Detection
Browser Request (example.nounspace.com)
↓
Server Component (layout.tsx, page.tsx, etc.)
├─ Calls await loadSystemConfig() ← SERVER-ONLY
│ ├─ Reads host header directly (async headers() API)
│ ├─ Normalizes domain
│ ├─ Resolves community ID via community_domains table or domain fallback
│ └─ Loads config from database
├─ Passes systemConfig as prop to Client Components
↓
Client Components
├─ Receive systemConfig prop
└─ Use config data (brand, assets, navigation, etc.)
↓
Renders with community-specific config
Key Point: Config loading is server-only. Client components never call loadSystemConfig() directly - they receive config via props.
Note: There is no Next.js middleware file. Domain detection happens directly in loadSystemConfig() using Next.js headers() API, which reads request headers.
Key Files:
src/config/index.ts- Main config loader entry pointsrc/config/loaders/registry.ts- Domain → community ID resolution and database queriessrc/config/loaders/runtimeLoader.ts- Runtime config loader implementation
2. Configuration Loading System
Server-Only Architecture
Important: loadSystemConfig() is server-only and can only be called from Server Components or Server Actions. Client components receive config via props.
Configuration Loader Architecture
loadSystemConfig(context?) - SERVER-ONLY
↓
Priority 1: Explicit context.communityId (if provided)
└─ Directly loads config for that community ID
↓
Priority 2: Domain resolution (if domain provided or detected from headers)
├─ Reads host header using Next.js headers() API
├─ Normalizes domain
├─ Resolves community ID via resolveCommunityIdFromDomain():
│ ├─ Checks community_domains table (database mapping)
│ └─ Falls back to domain as community_id (legacy)
└─ Loads config for resolved community ID
↓
Priority 3: Development override (NEXT_PUBLIC_TEST_COMMUNITY)
└─ Loads config for test community (dev only)
↓
Priority 4: Default fallback
└─ Loads config for DEFAULT_COMMUNITY_ID ('nounspace.com')
↓
RuntimeConfigLoader.load(context)
├─ Queries community_configs table
├─ Transforms database row to SystemConfig format
├─ Loads domain mappings from community_domains table
├─ Merges with shared themes (from shared/themes.ts)
└─ Returns SystemConfig
Prop Passing Pattern
Server Component (loads config)
↓ systemConfig prop
Client Wrapper Component
↓ systemConfig prop
Client Component (uses config)
↓ systemConfig prop (if needed)
Child Client Components
Example:
// ✅ CORRECT: Server Component
export default async function RootLayout() {
const systemConfig = await loadSystemConfig(); // Server-only
return <ClientComponent systemConfig={systemConfig} />;
}
// ❌ WRONG: Client Component
"use client";
export function MyComponent() {
const config = loadSystemConfig(); // ERROR: Can't use server APIs
}
Key Files:
src/config/index.ts- Main config loading entry pointsrc/config/loaders/runtimeLoader.ts- Database config loadersrc/config/loaders/types.ts- Type definitionssrc/config/loaders/utils.ts- Utility functions
Community ID Resolution Priority
- Explicit Context (
context.communityId) - Highest priority, directly loads config - Domain Resolution - Reads host header using Next.js
headers()API- Database Domain Mappings (checked first) -
community_domainstable lookup- Supports
blank_subdomain(e.g.,example.blank.space) - Supports
customdomains (e.g.,example.com)
- Supports
- Legacy Fallback - Domain as community_id (e.g.,
example.nounspace.com→example.nounspace.com)
- Database Domain Mappings (checked first) -
- Development Override (
NEXT_PUBLIC_TEST_COMMUNITY) - For local testing only - Default Fallback - Falls back to
DEFAULT_COMMUNITY_ID('nounspace.com')
Note: The system uses a database table (community_domains) for domain mappings, not hardcoded maps. This allows dynamic domain configuration without code changes.
Domain Resolution Process:
// 1. Normalize domain
const normalizedDomain = normalizeDomain(host);
// 2. Check community_domains table
const { data } = await supabase
.from('community_domains')
.select('community_id')
.eq('domain', normalizedDomain)
.maybeSingle();
// 3. Use mapped community_id or fall back to domain as community_id
const communityId = data?.community_id || normalizedDomain;
Database Domain Mappings:
Domain mappings are stored in the community_domains table:
- Each community can have one
blank_subdomain(e.g.,example.blank.space) - Each community can have one
customdomain (e.g.,example.com) - Domains are normalized before lookup
- Falls back to using domain as community_id if no mapping exists
3. Database Schema
community_configs Table
CREATE TABLE community_configs (
id UUID PRIMARY KEY,
community_id VARCHAR(50) NOT NULL UNIQUE,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
brand_config JSONB NOT NULL, -- Brand identity
assets_config JSONB NOT NULL, -- Asset paths
community_config JSONB NOT NULL, -- Community integration
fidgets_config JSONB NOT NULL, -- Enabled/disabled fidgets
navigation_config JSONB, -- Navigation items (with spaceId refs)
ui_config JSONB, -- UI colors
admin_identity_public_keys TEXT[], -- Admin public keys for navigation editing
is_published BOOLEAN DEFAULT true,
custom_domain_authorized BOOLEAN DEFAULT false, -- Custom domain authorization flag
admin_email TEXT -- Admin contact email
);
community_domains Table
CREATE TABLE community_domains (
id UUID PRIMARY KEY,
community_id VARCHAR(50) NOT NULL REFERENCES community_configs(community_id) ON DELETE CASCADE,
domain TEXT NOT NULL UNIQUE,
domain_type TEXT NOT NULL CHECK (domain_type IN ('blank_subdomain', 'custom')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
UNIQUE(community_id, domain_type) -- One blank_subdomain and one custom per community
);
Key Features:
- Maps domains to community IDs (replaces hardcoded mappings)
- Supports both blank subdomains (
*.blank.space) and custom domains - Each community can have one of each domain type
- Used for domain-based community resolution
- Public read access for domain resolution
Config Loading Process
The system no longer uses an RPC function. Instead, config loading happens in application code:
- Query
community_configstable directly - Transform database row to
SystemConfigformat in application code - Load domain mappings from
community_domainstable - Merge with shared themes from
shared/themes.ts - Return complete
SystemConfig
Key Features:
- Deterministic ordering by
updated_at DESC - Only returns published configs
- Returns most recent version if multiple exist
- Domain mappings loaded separately and merged into config
4. Configuration Structure
SystemConfig Interface
interface SystemConfig {
brand: BrandConfig; // From database
assets: AssetConfig; // From database
theme: ThemeConfig; // From shared/themes.ts (NOT in database)
community: CommunityConfig; // From database
fidgets: FidgetConfig; // From database
navigation?: NavigationConfig; // From database (with spaceId refs)
ui?: UIConfig; // From database
adminIdentityPublicKeys?: string[]; // From database (admin public keys)
communityId: string; // Database community_id (added for API operations)
}
What's Stored Where
| Component | Storage Location | Notes |
|---|---|---|
| Brand Config | Database (brand_config) | Display name, description, mini-app tags |
| Assets Config | Database (assets_config) | Logo paths, favicon, OG images |
| Community Config | Database (community_config) | URLs, social handles, governance identifiers, tokens |
| Fidgets Config | Database (fidgets_config) | Enabled/disabled fidget IDs |
| Navigation Config | Database (navigation_config) | Navigation items with spaceId refs |
| UI Config | Database (ui_config) | Primary colors, hover states, font colors, font URL |
| Themes | src/config/shared/themes.ts | Shared across all communities |
| Navigation Pages | Supabase Storage (spaces bucket) | Stored as Spaces, referenced by spaceId |
| Community ID | SystemConfig (runtime) | Database community_id passed through for API operations |
Note: The communityId field in SystemConfig is the database community_id (e.g., "nounspace.com", "nouns") used for API operations. This is different from community.type which is a semantic descriptor (e.g., "nouns", "token_platform") stored in the community_config JSONB field.
5. Navigation Pages as Spaces
Concept
Navigation pages (like /home and /explore) are not stored in the database config. Instead, they are stored as Spaces in Supabase Storage and referenced by navigation items via spaceId.
Navigation Item Structure
interface NavigationItem {
id: string;
label: string;
href: string; // e.g., "/home"
icon?: string; // react-icons name (e.g., "FaHome") or custom URL
spaceId?: string; // Reference to Space in storage
}
Space Storage Structure
spaces/
{spaceId}/
tabOrder ← JSON: { tabOrder: ["Nouns", "Socials", ...] }
tabs/
Nouns ← SpaceConfig JSON (fidgets, layout, etc.)
Socials ← SpaceConfig JSON
...
Loading Flow
User navigates to /home
↓
Middleware: Sets x-community-id header
↓
NavPage Server Component (page.tsx)
├─ Step 1: Load SystemConfig
│ └─ Gets navigation items from database
│ └─ Finds nav item: { href: "/home", spaceId: "abc-123-def" }
│
├─ Step 2: Load Space from Storage
│ └─ Downloads: spaces/abc-123-def/tabOrder
│ └─ Downloads: spaces/abc-123-def/tabs/Nouns
│ └─ Downloads: spaces/abc-123-def/tabs/Socials
│ └─ Constructs NavPageConfig
│
├─ Step 3: Redirect to default tab (if no tab specified)
│ └─ Redirects to: /home/Nouns
│
└─ Step 4: Render with tab
└─ Passes NavPageConfig to NavPageClient
↓
NavPageClient (Client Component)
├─ Receives: pageConfig, activeTabName, navSlug props
├─ Extracts active tab config from pageConfig.tabs
├─ Creates TabBar component
└─ Renders SpacePage with tab config
Detailed Navigation Page Flow
When user visits /home:
-
Middleware runs first:
- Detects domain:
example.nounspace.com - Sets header:
x-community-id: "example"
- Detects domain:
-
NavPage Server Component:
- Loads
SystemConfig→ gets navigation items - Finds nav item with
href="/home"→ extractsspaceId - Loads Space from Supabase Storage:
- Downloads
tabOrderfile - Downloads each tab config file
- Downloads
- Constructs
NavPageConfigobject - If no tab specified → redirects to
/home/{defaultTab}
- Loads
-
NavPage runs again with tab:
- Loads
SystemConfigagain - Loads Space from Storage again
- Validates tab exists
- Passes
NavPageConfigtoNavPageClient
- Loads
-
NavPageClient (Client Component):
- Receives
pageConfigprop (NavPageConfig) - Extracts active tab config
- Renders
SpacePagewith tab content
- Receives
Storage Structure:
Supabase Storage (spaces bucket):
spaces/
{spaceId}/
tabOrder ← SignedFile: { tabOrder: ["Nouns", "Socials"] }
tabs/
Nouns ← SignedFile: SpaceConfig JSON
Socials ← SignedFile: SpaceConfig JSON
Database References:
// In community_configs.navigation_config:
{
"items": [
{
"id": "home",
"href": "/home",
"spaceId": "abc-123-def" ← References Space in storage
}
]
}
Key Files:
src/app/[navSlug]/[[...tabName]]/page.tsx- Dynamic navigation page handlersrc/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx- Client component for renderingsrc/config/systemConfig.ts-NavPageConfigtype definition
Related Documentation:
- See Navigation System for details on the navigation editor
- See Space Architecture for details on how Spaces work
- See Public Spaces Pattern for the server/client separation pattern
6. Space Creators
Simplified Architecture
All communities now use Nouns implementations for initial space creation. The space creator functions are synchronous and directly re-export Nouns implementations.
// All communities use Nouns implementations
export const createInitialProfileSpaceConfigForFid = nounsCreateInitialProfileSpaceConfigForFid;
export const createInitialChannelSpaceConfig = nounsCreateInitialChannelSpaceConfig;
export const createInitialTokenSpaceConfigForAddress = nounsCreateInitialTokenSpaceConfigForAddress;
export const createInitalProposalSpaceConfigForProposalId = nounsCreateInitalProposalSpaceConfigForProposalId;
export const INITIAL_HOMEBASE_CONFIG = nounsINITIAL_HOMEBASE_CONFIG;
Key Files:
src/config/index.ts- Re-exports Nouns implementationssrc/config/nouns/initialSpaces/- Nouns space creator implementations
Key Architectural Changes
Removed Components
- Build-Time Config Loading - All configs now load at runtime
- Static Config Fallbacks - No fallback to TypeScript configs
- Community-Specific Space Creators - All use Nouns implementations
- HomePageConfig/ExplorePageConfig in SystemConfig - Moved to Spaces
- Factory Pattern for Config Loaders - Simplified to single runtime loader
Added Components
- Direct Header-Based Domain Detection - Domain resolution using Next.js
headers()API - Runtime Database Loading - All configs from Supabase
- Navigation-Space References - Pages stored as Spaces
- NavPageConfig Type - Unified type for navigation pages
- community_domains Table - Database-backed domain mappings (replaces hardcoded maps)
- Application-Level Config Transformation - Config transformation in code, not RPC function
Simplified Components
- Config Loading - Single
RuntimeConfigLoader(no factory) - Space Creators - Synchronous, Nouns-only implementations
- Type System -
NavPageConfigreplacesHomePageConfig | ExplorePageConfig - Community Resolution - Clear priority order
Data Flow Examples
Example 1: Loading Config for example.nounspace.com
1. Server Component calls loadSystemConfig()
└─ Reads host header: "example.nounspace.com"
└─ Normalizes domain: "example.nounspace.com"
2. Domain Resolution
└─ Checks community_domains table for "example.nounspace.com"
└─ If not found, uses domain as community_id: "example.nounspace.com"
└─ If found, uses mapped community_id from table
3. Config Loading
└─ Queries community_configs table for resolved community_id
└─ Transforms database row to SystemConfig format
└─ Loads domain mappings from community_domains table
└─ Merges with themes from shared/themes.ts
└─ Returns SystemConfig
4. Component renders with example community config
Example 2: Navigating to /home Page
1. Request: example.nounspace.com/home
└─ Server Component reads host header: "example.nounspace.com"
2. NavPage Server Component loads
└─ Calls: await loadSystemConfig()
└─ Gets navigation items from database
└─ Finds: { href: "/home", spaceId: "abc-123-def" }
3. NavPage loads Space from Supabase Storage
└─ Downloads: spaces/abc-123-def/tabOrder
└─ Downloads: spaces/abc-123-def/tabs/Nouns
└─ Downloads: spaces/abc-123-def/tabs/Socials
└─ Constructs NavPageConfig:
{
defaultTab: "Nouns",
tabOrder: ["Nouns", "Socials"],
tabs: { "Nouns": {...}, "Socials": {...} }
}
4. Redirects to default tab: /home/Nouns
5. NavPage runs again with tab
└─ Loads SystemConfig and Space again
└─ Validates "Nouns" tab exists
└─ Passes NavPageConfig to NavPageClient
6. NavPageClient (Client Component) renders
└─ Receives pageConfig prop
└─ Extracts tab config: pageConfig.tabs["Nouns"]
└─ Creates TabBar component
└─ Renders SpacePage with tab content
Example 3: Component Hierarchy & Prop Flow
RootLayout (Server Component)
├─ await loadSystemConfig() ← SERVER-ONLY
├─ ClientMobileHeaderWrapper (Client)
│ └─ systemConfig prop
│ └─ MobileHeader (Client)
│ ├─ systemConfig prop
│ ├─ BrandHeader (Client) ← uses systemConfig.assets
│ └─ Navigation (Client) ← uses systemConfig.navigation
│
└─ ClientSidebarWrapper (Client)
└─ systemConfig prop
└─ Sidebar (Client)
└─ systemConfig prop
└─ Navigation (Client) ← uses systemConfig.navigation
Environment Variables
Required
NEXT_PUBLIC_SUPABASE_URL- Supabase project URLNEXT_PUBLIC_SUPABASE_ANON_KEY- Supabase anon key (for runtime loading)SUPABASE_SERVICE_KEY- Service role key (for seeding/admin operations)
Optional
NEXT_PUBLIC_TEST_COMMUNITY- Override for local testing (development only)
Testing & Development
Local Testing
- Localhost Subdomains:
example.localhost:3000→ detects "example" - Environment Override:
NEXT_PUBLIC_TEST_COMMUNITY=example npm run dev
Note: If neither method is used, the system will error when attempting to load config. Always set NEXT_PUBLIC_TEST_COMMUNITY or use localhost subdomains in development.
Production
- Domain-based detection:
example.nounspace.com→ "example" - Requires valid domain resolution (no fallback)
Benefits
- Multi-Tenant Support - Single deployment serves multiple communities
- Dynamic Updates - Config changes without rebuild
- Domain-Based Routing - Automatic community detection
- Unified Architecture - Pages are Spaces, consistent with existing system
- Shared Themes - Single source of truth, no duplication
- Simplified Codebase - Removed build-time complexity
- Deterministic Loading - Database function orders by
updated_at DESC - Server-Client Separation - Clear boundaries, no client-side server API calls
- Type Safety - SystemConfig type flows through props
- Performance - Config loaded once at root, reused throughout app
- No Hydration Issues - No client-side domain detection
Related Files
Core Configuration
src/config/index.ts- Main config loadersrc/config/systemConfig.ts- Type definitionssrc/config/loaders/runtimeLoader.ts- Database loadersrc/config/loaders/utils.ts- Utility functionssrc/config/loaders/registry.ts- Domain resolutionsrc/config/shared/themes.ts- Shared themes
Routing & Navigation
src/app/[navSlug]/[[...tabName]]/page.tsx- Dynamic navigation (Server Component)src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx- Client component for renderingsrc/app/layout.tsx- Root layout that loads config and passes to client componentssrc/common/components/organisms/ClientSidebarWrapper.tsx- Client wrapper for Sidebarsrc/common/components/organisms/ClientMobileHeaderWrapper.tsx- Client wrapper for MobileHeadersrc/common/components/organisms/navigation/NavigationEditor.tsx- Navigation editor UIsrc/common/components/organisms/navigation/useNavigation.ts- Navigation management hooksrc/common/data/stores/app/navigation/navigationStore.ts- Navigation Zustand storesrc/pages/api/navigation/config.ts- Navigation config API endpoint
Database
supabase/migrations/20251129172847_create_community_configs.sql- Community configs schemasupabase/migrations/20260215000000_add_community_domains_and_domain_fields.sql- Domain mappings schemascripts/seed.ts- Unified seeding script (replaces all individual seed scripts)
Space Creators
src/config/nouns/initialSpaces/- Nouns implementationssrc/config/index.ts- Re-exports
Future Considerations
- Versioning: Database function supports multiple versions (orders by
updated_at) - Admin UI: Navigation editor provides admin interface for navigation config updates (see Navigation System)
- Validation: Could add JSON schema validation for configs
- Rollback: Could add version history and rollback capabilities