Widget Data Architecture
This skill guides you through StickerNest's data architecture, where widgets don't store data - they display data.
Core Principle: Data/View Separation
WRONG: Character Widget = Character Data + Character View (tightly coupled) RIGHT: Data Schemas + View Widgets (loosely coupled) DATA SCHEMAS VIEW WIDGETS (What is stored) (How it's displayed) ├─ Characters ├─ Wiki View ├─ Inventory ├─ Card Grid View ├─ B-Roll ├─ Table View ├─ Locations ├─ Kanban View ├─ (user-defined) ├─ Timeline View └─ (external/Notion) └─ (community-created) Connect any schema to any compatible view.
Data Source Tiers
Tier 1: Core Tables (Platform-Provided)
Built-in, optimized schemas for common use cases:
// Core schemas defined by platform const CORE_SCHEMAS = [ 'core.asset', // Files, images, videos 'core.event', // Calendar events 'core.bookmark', // Links, references 'core.note', // Simple text + metadata 'core.inventory', // Item collections 'core.character', // Character profiles ];
Tier 2: User Schemas (User-Defined)
Users define custom schemas via the schema registry:
// User creates a custom schema
const brollSchema = await createSchema({
namespace: 'editor', // or 'user_123' for private
name: 'broll_library',
fields: [
{ name: 'clip_name', type: 'text', required: true, indexed: true },
{ name: 'duration', type: 'number', required: true },
{ name: 'tags', type: 'text[]', indexed: true },
{ name: 'thumbnail', type: 'asset_ref' },
{ name: 'source', type: 'asset_ref', required: true },
{ name: 'notes', type: 'text' },
],
visibility: 'private', // 'private' | 'unlisted' | 'public'
});
Tier 3: External Bridges (Connected Services)
Connect to data in existing tools:
| Service | Auth | Data Flow | Use Case |
|---|---|---|---|
| Notion | OAuth | Read/Write | Databases, wikis |
| Obsidian | Local/Plugin | Sync | Notes, knowledge graphs |
| Airtable | OAuth | Read/Write | Structured databases |
| Google Sheets | OAuth | Read/Write | Spreadsheets |
| Spotify | OAuth | Read | Playlists, audio features |
| GitHub | OAuth | Read/Write | Code, issues |
Tier 4: Hybrid (Cached External)
Sync external data locally for offline/performance:
// External data cached locally
const hybridConfig = {
source: 'notion.database_abc123',
cache: {
strategy: 'sync',
staleness: 'minutes', // 'realtime' | 'minutes' | 'hours' | 'daily'
conflictResolution: 'external_wins',
},
};
Schema Definition
Field Types
type FieldType = | 'text' // String | 'richtext' // Formatted text, markdown | 'number' // Integer or float | 'boolean' // True/false | 'date' // Date only | 'datetime' // Date + time | 'enum' // Single select from options | 'text[]' // Array of strings (tags) | 'number[]' // Array of numbers | 'asset_ref' // Reference to uploaded asset | 'url' // URL string | 'email' // Email string | 'relationship' // Reference to another schema record | 'relationship[]' // Array of references | 'json' // Arbitrary JSON object
Schema Structure
interface SchemaDefinition {
// Identity
namespace: string; // 'author', 'editor', 'user_123'
name: string; // 'story_characters'
version: number; // Schema version for migrations
// Fields
fields: FieldDefinition[];
// Indexes for query performance
indexes?: IndexDefinition[];
// Relationships to other schemas
relationships?: RelationshipDefinition[];
// Access control
visibility: 'private' | 'unlisted' | 'public';
allowForks: boolean;
}
interface FieldDefinition {
name: string; // 'character_name'
type: FieldType;
required: boolean;
indexed?: boolean; // Create database index
default?: any; // Default value
options?: string[]; // For enum types
target?: string; // For relationship types: 'author.settings'
description?: string; // For UI and AI context
}
Example: Author's Character Database
const characterSchema: SchemaDefinition = {
namespace: 'author',
name: 'story_characters',
version: 1,
fields: [
{ name: 'name', type: 'text', required: true, indexed: true },
{ name: 'role', type: 'enum', required: true,
options: ['protagonist', 'antagonist', 'supporting', 'minor'] },
{ name: 'backstory', type: 'richtext', required: false },
{ name: 'traits', type: 'text[]', required: false },
{ name: 'age', type: 'number', required: false },
{ name: 'portrait', type: 'asset_ref', required: false },
{ name: 'notes', type: 'text', required: false },
],
relationships: [
{
name: 'relationships',
target: 'author.story_characters', // Self-referential
type: 'many_to_many',
metadata: { name: 'relation_type', type: 'text' }, // "friend", "enemy"
},
{
name: 'appears_in',
target: 'author.scenes',
type: 'many_to_many',
},
],
visibility: 'private',
allowForks: false,
};
Widget Data Binding
View Widget Requirements
View widgets declare what data shape they can display:
// In widget manifest
dataBinding: {
// Required fields - widget won't work without these
requiredFields: [
{ role: 'title', types: ['text'], description: 'Display title' },
{ role: 'content', types: ['text', 'richtext', 'markdown'] },
],
// Optional fields - widget can use if available
optionalFields: [
{ role: 'image', types: ['asset_ref', 'url'] },
{ role: 'tags', types: ['text[]'] },
{ role: 'date', types: ['date', 'datetime'] },
{ role: 'author', types: ['text', 'relationship'] },
],
// Features the widget supports
features: ['search', 'filter', 'sort', 'edit_inline', 'pagination'],
}
Field Mapping
When connecting a widget to data, users map schema fields to widget roles:
// User binds "Wiki View" widget to "Story Characters" schema
const binding: DataBinding = {
schemaId: 'author.story_characters',
// Map schema fields to widget roles
mappings: {
title: { sourceField: 'name' },
content: { sourceField: 'backstory' },
image: { sourceField: 'portrait' },
tags: { sourceField: 'traits' },
},
// Filter what records to show
filter: { role: { $in: ['protagonist', 'antagonist'] } },
// Sort order
sort: { field: 'name', direction: 'asc' },
};
Connecting Widget to Data (User Flow)
1. User adds "Wiki View" widget to canvas 2. System prompts: "Select data source" 3. User sees: - My Schemas (author.story_characters, author.scenes...) - Connected (Notion: "Characters DB", Obsidian: "/Characters") - Community (shared schemas) 4. User selects "author.story_characters" 5. System shows field mapping UI: - Title (required) → [name ▼] - Content (required) → [backstory ▼] - Image (optional) → [portrait ▼] - Tags (optional) → [traits ▼] 6. User confirms, widget displays the data
External Bridges
Bridge Widget Pattern
Bridge widgets connect to external services and expose their data:
interface BridgeWidgetManifest extends WidgetManifest {
type: 'bridge.notion' | 'bridge.obsidian' | 'bridge.airtable' | ...;
bridge: {
service: string;
auth: 'oauth' | 'api_key' | 'local_file';
dataFlow: 'read_only' | 'read_write' | 'sync';
exposedSchema: SchemaDefinition | 'dynamic';
};
}
Notion Bridge Example
const notionBridgeManifest: BridgeWidgetManifest = {
type: 'bridge.notion',
name: 'Notion Database Bridge',
version: '1.0.0',
bridge: {
service: 'notion',
auth: 'oauth',
dataFlow: 'read_write',
exposedSchema: 'dynamic', // Inferred from Notion DB structure
},
// What this bridge exposes to other widgets
exposes: {
capabilities: [
{ name: 'query', description: 'Query the Notion database' },
{ name: 'create', description: 'Create new entries' },
{ name: 'update', description: 'Update existing entries' },
],
events: [
{ name: 'data:changed', description: 'When Notion data changes' },
],
},
};
Bridge Caching Strategy
interface BridgeCacheConfig {
// How fresh must data be?
staleness: 'realtime' | 'minutes' | 'hours' | 'daily';
// Cache location
cacheLayer: 'memory' | 'supabase' | 'both';
// Sync strategy
sync: {
pullInterval?: number; // ms, or null for manual
pushStrategy: 'immediate' | 'batched' | 'manual';
conflictResolution: 'external_wins' | 'local_wins' | 'manual';
};
}
Unified Data API
All data sources (native, user, external) use the same API:
// Query any data source
const results = await dataAPI.query({
source: 'author.story_characters', // or 'notion.db_abc123'
filter: { role: 'protagonist' },
sort: { field: 'name', direction: 'asc' },
limit: 20,
offset: 0,
});
// Insert data
await dataAPI.insert({
source: 'author.story_characters',
data: { name: 'New Character', role: 'supporting' },
});
// Update data
await dataAPI.update({
source: 'author.story_characters',
id: 'record_123',
data: { backstory: 'Updated backstory...' },
});
// Subscribe to changes
dataAPI.subscribe({
source: 'author.story_characters',
onChange: (changes) => { /* handle updates */ },
});
AI Access to Data
AI widgets access data through the same system, with permissions:
// AI widget permission request
{
permissions: {
databaseRead: [
{
source: 'author.story_characters',
fields: ['name', 'role', 'backstory', 'traits'],
filter: 'user_id = $current_user',
justification: 'To help develop your characters',
},
],
databaseWrite: [
{
source: 'ai_assistant.memory', // AI's own memory DB
fields: '*',
justification: 'To remember our conversation',
},
],
},
}
Schema Registry Implementation
Database Tables
-- Schema definitions CREATE TABLE widget_schema_registry ( id UUID PRIMARY KEY, creator_id UUID NOT NULL, namespace TEXT NOT NULL, name TEXT NOT NULL, version INT DEFAULT 1, fields JSONB NOT NULL, indexes JSONB, relationships JSONB, visibility TEXT DEFAULT 'private', allow_forks BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(namespace, name) ); -- User data storage (validated against schema) CREATE TABLE widget_custom_data ( id UUID PRIMARY KEY, user_id UUID NOT NULL, canvas_id UUID, schema_id UUID REFERENCES widget_schema_registry(id), schema_type TEXT NOT NULL, -- Denormalized: 'author.story_characters' data JSONB NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Indexes CREATE INDEX idx_custom_data_user ON widget_custom_data(user_id); CREATE INDEX idx_custom_data_schema ON widget_custom_data(schema_type); CREATE INDEX idx_custom_data_json ON widget_custom_data USING GIN (data);
Best Practices
For Schema Design
- •Use meaningful field names -
character_namenotcn - •Add descriptions - Help AI and UI understand fields
- •Index query fields - Fields used in filters/sorts
- •Use relationships - Don't embed IDs in text fields
- •Version schemas - For future migrations
For Widget Data Binding
- •Declare all required fields - Don't fail silently
- •Use standard roles -
title,content,image,tags,date - •Support missing optional fields - Graceful degradation
- •Emit data change events - Let other widgets react
For External Bridges
- •Cache aggressively - External APIs are slow
- •Handle offline - Graceful degradation when disconnected
- •Respect rate limits - Queue and batch requests
- •Show sync status - Users need to know if data is fresh
Reference Files
- •Vision document:
docs/architecture/GAME_ENGINE_CANVAS_VISION.md - •Schema types:
src/types/schema.ts - •Data API:
src/runtime/DataAPI.ts - •Bridge implementations:
src/widgets/bridges/