Documentation

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:

FieldTypePurpose
stagestringOptional lifecycle marker used by node headers
playlistFlowPlaylistOrdered playlist structure for flow sequencing with support for inline branching
platformNotesPartial<Record<PlatformId, string>>Per-platform notes in the detail panel
platformStatusesPartial<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: localStorage with an in-memory Map<string, ProjectBundle>
  • Indexing: Dual index maps — nodeIndex (node ID → project ID) and edgeIndex (edge ID → project ID) for fast lookups
  • Persistence: Auto-persists to localStorage on every mutation
  • Cascade: deleteNode also removes all edges referencing that node
  • Normalization: Legacy structural fields from pre-playlist storage are stripped on load/import, and ordered metadata.playlist.entries values are hydrated from legacy data when present

Import / Export

Utilities in lib/utils/export.ts:

  • exportToJson(bundle) — Serializes a ProjectBundle to formatted JSON
  • downloadJson(bundle) — Triggers browser download as {project-title-slug}-{projectId}.json and returns export diagnostics (filename, bytes, warning)
  • exportProject(id) / importProject(bundle) — Delegate to localProvider
  • importProjectFromFile(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:

AssetPathPurpose
ProjectBundle schemapublic/schema/project-bundle.jsonCanonical JSON Schema for the bundle format
Example bundlepublic/schema/example-bundle.jsonComplete, 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:

HookReturnsPurpose
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:

  1. Current: localStorage via localProvider
  2. 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.