Data Layer
Data Types
Defined in lib/data/types.ts:
Node
interface Node {
id: string;
project_id: string;
species: SpeciesId;
title: string;
description?: string;
status: StatusId;
platforms: PlatformId[];
metadata?: NodeMetadata;
}
NodeMetadata
Defined in lib/data/types.ts:
| Field | Type | Purpose |
|---|---|---|
stage | string | Optional lifecycle marker used by node headers |
playlist | FlowPlaylist | Ordered playlist structure for flow sequencing with support for inline branching |
platformNotes | Partial<Record<PlatformId, string>> | Per-platform notes in the detail panel |
platformStatuses | Partial<Record<PlatformId, StatusId>> | Per-platform source-of-truth statuses for views |
FlowPlaylist structure:
type PlaylistEntry =
| { type: "view"; view_id: string }
| { type: "flow"; flow_id: string }
| { type: "condition"; label: string; if_true: PlaylistEntry[]; if_false: PlaylistEntry[] }
| { type: "junction"; label: string; cases: JunctionCase[] };
interface JunctionCase {
label: string;
entries: PlaylistEntry[];
}
interface FlowPlaylist {
entries: PlaylistEntry[];
}
Edge
interface Edge {
id: string;
project_id: string;
source_id: string;
target_id: string;
edge_type: EdgeTypeId;
metadata?: Record<string, unknown>;
}
Project
interface Project {
id: string;
title: string;
description?: string;
root_node_id?: string; // Optional node id used as the canvas anchor
metadata?: ProjectMetadata; // Optional project-level UI preferences
created_at: string; // ISO 8601
updated_at: string; // ISO 8601
archived_at?: string | null; // ISO 8601 when archived
}
interface ProjectMetadata {
view_card_variant?: "compact" | "large";
}
ProjectBundle
interface ProjectBundle {
project: Project;
nodes: Node[];
edges: Edge[];
}
A ProjectBundle is the unit of storage and export — one project with all its nodes and edges.
project.root_node_id (when present) points to the node that should render as the primary canvas anchor in app/project/[id]/canvas/page.tsx. If it is missing, the canvas falls back to inferred roots (nodes without compose parents).
DataProvider Interface
Defined in lib/data/data-provider.ts. All data access goes through this interface:
interface DataProvider {
// Projects
getProject(id: string): Promise<ProjectBundle | undefined>;
listProjects(): Promise<ProjectBundle[]>;
saveProject(bundle: ProjectBundle): Promise<void>;
archiveProject(id: string): Promise<void>;
// Nodes
getNodes(projectId: string): Promise<Node[]>;
createNode(node: Node): Promise<Node>;
updateNode(id: string, patch: Partial<Omit<Node, "id" | "project_id">>): Promise<Node>;
deleteNode(id: string): Promise<void>;
// Edges
getEdges(projectId: string): Promise<Edge[]>;
createEdge(edge: Edge): Promise<Edge>;
deleteEdge(id: string): Promise<void>;
// Import/Export
exportProject(id: string): Promise<ProjectBundle>;
importProject(bundle: ProjectBundle): Promise<Project>;
}
archiveProject performs a soft delete. Archived projects remain in storage but are excluded by default from listProjects().
Local Provider
Implemented in lib/data/local-provider.ts.
- Storage key:
arkaik:store - Backend:
localStoragewith an in-memoryMap<string, ProjectBundle> - Indexing: Dual index maps —
nodeIndex(node ID → project ID) andedgeIndex(edge ID → project ID) for fast lookups - Persistence: Auto-persists to
localStorageon every mutation - Cascade:
deleteNodealso removes all edges referencing that node - Normalization: Legacy structural fields from pre-playlist storage are stripped on load/import, and ordered
metadata.playlist.entriesvalues are hydrated from legacy data when present
Import / Export
Utilities in lib/utils/export.ts:
exportToJson(bundle)— Serializes aProjectBundleto formatted JSONdownloadJson(bundle)— Triggers browser download as{project-title-slug}-{projectId}.jsonand returns export diagnostics (filename,bytes,warning)exportProject(id)/importProject(bundle)— Delegate tolocalProviderimportProjectFromFile(file)— Parses and validates JSON file content, normalizes timestamps, and imports via provider
downloadJson(bundle) applies a soft warning when the serialized bundle is larger than 4 MB. The warning is intended for UX guidance only and does not block download.
When importing, if the incoming project ID already exists locally, a new project ID is generated and all project_id references in nodes and edges are rewritten to the new ID before saving.
When importing JSON, project.root_node_id is optional. If provided, it must reference an existing node ID in nodes or the import fails validation.
Public Schema Contract
Arkaik now publishes a machine-readable schema and example bundle for import/export alignment and LLM prompt tooling:
| Asset | Path | Purpose |
|---|---|---|
| ProjectBundle schema | public/schema/project-bundle.json | Canonical JSON Schema for the bundle format |
| Example bundle | public/schema/example-bundle.json | Complete, valid reference example |
These assets mirror the TypeScript model in lib/data/types.ts and help external tooling generate importable bundles.
Hooks
Hooks in lib/hooks/ provide React state wrappers around the provider:
| Hook | Returns | Purpose |
|---|---|---|
useProject(id) | { project, loading, updateProject } | Load and update project-level metadata/settings |
useProjects() | { projects, loading } | Load the active project list for shell navigation |
useNodes(projectId) | { nodes, loading, addNode, removeNode, updateNode } | CRUD for nodes |
useEdges(projectId) | { edges, loading, addEdge, removeEdge } | CRUD for edges |
useGraphNavigation() | { expandedNodeIds, zoomLevel, breadcrumbs, expand, collapse, navigateTo } | Generic graph navigation state utility |
The project canvas page (app/project/[id]/canvas/page.tsx) uses useProject for root-node anchoring and project-level card-style preferences, and still manages expandedFlows as local state.
Node Editing Flow
The NodeDetailPanel is the primary UI for editing nodes. The mutation path:
NodeDetailPanel (title, description, platforms, metadata)
→ useNodes.updateNode(id, patch)
→ localProvider.updateNode(id, patch)
→ localStorage
Views store editable per-platform statuses in node.metadata.platformStatuses. When legacy data does not have that field yet, the UI derives platform statuses from node.status + node.platforms and writes the richer metadata shape back on the next edit.
Flows do not expose an editable rollup status in UI. Flow cards and panel gauges compute status from descendant views in app/project/[id]/canvas/page.tsx and components/panels/NodeDetailPanel.tsx.
Flow playlist edits (metadata.playlist.entries) also originate from NodeDetailPanel via components/panels/PlaylistEditor.tsx. All playlist mutations use useNodes.updateNode, and provider-side validation blocks circular flow references before persistence.
Migration Path
The DataProvider interface abstracts storage so the backend can change without touching hooks or UI:
- Current:
localStoragevialocalProvider - Planned: Supabase (auth, RLS, realtime sync)
To add a new provider: implement the DataProvider interface and swap the import in the hooks.
Seed Data
seed/pebbles.json contains an example project ("Pebbles") demonstrating the 4-species model, persisted compose edges for structure, and playlist-driven flow ordering.