oneie
Version:
Build apps, websites, and AI agents in English. Zero-interaction setup for AI agents (Claude Code, Cursor, Windsurf). Download to your computer, run in the cloud, deploy to the edge. Open source and free forever.
1,935 lines (1,606 loc) • 66.6 kB
Markdown
---
title: 2 1 Dataprovider Interface
dimension: things
category: features
tags: ai, architecture, backend, frontend, ontology
related_dimensions: events, groups
scope: global
created: 2025-11-03
updated: 2025-11-03
version: 1.0.0
ai_context: |
This document is part of the things dimension in the features category.
Location: one/things/features/2-1-dataprovider-interface.md
Purpose: Documents feature 2-1: dataprovider interface & convexprovider
Related dimensions: events, groups
For AI agents: Read this to understand 2 1 dataprovider interface.
---
# Feature 2-1: DataProvider Interface & ConvexProvider
**Feature ID:** `feature_2_1_dataprovider_interface`
**Plan:** `plan_2_backend_agnostic_frontend`
**Owner:** Backend Specialist
**Status:** Detailed Specification Complete
**Priority:** P0 (Critical Path - Blocks All Other Features)
**Effort:** 1 week
**Dependencies:** None
---
## Assignment
This feature implements the **DataProvider interface** and **ConvexProvider implementation** that abstracts all backend operations behind a clean, typed API. This is the foundation for backend-agnostic frontend architecture.
**Goal:** Create a single interface that handles ALL 6-dimension ontology operations, allowing the frontend to remain completely decoupled from Convex-specific APIs.
---
## 1. Complete Technical Specification
### 1.1 DataProvider Interface
Complete TypeScript interface covering all 6 dimensions of the ontology:
```typescript
// frontend/src/providers/DataProvider.ts
import { Effect } from "effect";
import { Id } from "@/convex/_generated/dataModel";
/**
* DataProvider Interface - Complete abstraction over backend operations
*
* This interface represents the SINGLE source of truth for all data operations
* in the ONE platform. ANY backend can implement this interface.
*
* Design Principles:
* 1. Effect.ts-based error handling (typed errors)
* 2. Organization-scoped operations (multi-tenant by default)
* 3. Immutable operations (pure functions)
* 4. Type-safe parameters and return values
* 5. No backend-specific code leaks through this interface
*/
export interface DataProvider {
/**
* DIMENSION 1: Organizations
* Multi-tenant isolation boundary - who owns what at org level
*/
organizations: {
/**
* Get organization by ID
* @returns Effect that succeeds with Organization or fails with OrganizationNotFoundError
*/
get: (
id: Id<"organizations">,
) => Effect.Effect<Organization, OrganizationNotFoundError>;
/**
* List organizations (platform owner only)
* @returns Effect with array of organizations
*/
list: (
params: ListOrganizationsParams,
) => Effect.Effect<Organization[], Error>;
/**
* Create new organization
* @returns Effect with organization ID
*/
create: (
input: CreateOrganizationInput,
) => Effect.Effect<Id<"organizations">, Error>;
/**
* Update organization settings
* @returns Effect with updated organization
*/
update: (
id: Id<"organizations">,
updates: Partial<Organization>,
) => Effect.Effect<Organization, Error>;
/**
* Get organization usage stats
* @returns Effect with usage metrics
*/
getUsage: (
id: Id<"organizations">,
) => Effect.Effect<OrganizationUsage, Error>;
};
/**
* DIMENSION 2: People
* Authorization & governance - who can do what
*/
people: {
/**
* Get person by ID
* @returns Effect with Person or PersonNotFoundError
*/
get: (id: Id<"people">) => Effect.Effect<Person, PersonNotFoundError>;
/**
* List people in organization
* @returns Effect with array of people
*/
list: (params: ListPeopleParams) => Effect.Effect<Person[], Error>;
/**
* Create new person (user)
* @returns Effect with person ID
*/
create: (input: CreatePersonInput) => Effect.Effect<Id<"people">, Error>;
/**
* Update person profile
* @returns Effect with updated person
*/
update: (
id: Id<"people">,
updates: Partial<Person>,
) => Effect.Effect<Person, Error>;
/**
* Get person's organizations
* @returns Effect with array of organizations
*/
getOrganizations: (
id: Id<"people">,
) => Effect.Effect<Organization[], Error>;
/**
* Check person's permissions
* @returns Effect with boolean
*/
checkPermission: (
id: Id<"people">,
permission: string,
) => Effect.Effect<boolean, Error>;
};
/**
* DIMENSION 3: Things
* All entities - users, agents, content, tokens, courses
*/
things: {
/**
* Get thing by ID
* @returns Effect with Thing or ThingNotFoundError
*/
get: (id: Id<"things">) => Effect.Effect<Thing, ThingNotFoundError>;
/**
* List things by type and filters
* @returns Effect with array of things
*/
list: (params: ListThingsParams) => Effect.Effect<Thing[], Error>;
/**
* Create new thing
* @returns Effect with thing ID
*/
create: (input: CreateThingInput) => Effect.Effect<Id<"things">, Error>;
/**
* Update thing
* @returns Effect with updated thing
*/
update: (
id: Id<"things">,
updates: Partial<Thing>,
) => Effect.Effect<Thing, Error>;
/**
* Delete thing (soft delete)
* @returns Effect with void
*/
delete: (id: Id<"things">) => Effect.Effect<void, Error>;
/**
* Search things (full-text search)
* @returns Effect with search results
*/
search: (params: SearchThingsParams) => Effect.Effect<Thing[], Error>;
};
/**
* DIMENSION 4: Connections
* All relationships - owns, follows, holds_tokens, enrolled_in
*/
connections: {
/**
* Get connection by ID
* @returns Effect with Connection or ConnectionNotFoundError
*/
get: (
id: Id<"connections">,
) => Effect.Effect<Connection, ConnectionNotFoundError>;
/**
* List connections from a thing
* @returns Effect with array of connections
*/
listFrom: (
params: ListConnectionsFromParams,
) => Effect.Effect<Connection[], Error>;
/**
* List connections to a thing
* @returns Effect with array of connections
*/
listTo: (
params: ListConnectionsToParams,
) => Effect.Effect<Connection[], Error>;
/**
* Create connection between things
* @returns Effect with connection ID
*/
create: (
input: CreateConnectionInput,
) => Effect.Effect<Id<"connections">, Error>;
/**
* Update connection metadata
* @returns Effect with updated connection
*/
update: (
id: Id<"connections">,
updates: Partial<Connection>,
) => Effect.Effect<Connection, Error>;
/**
* Delete connection
* @returns Effect with void
*/
delete: (id: Id<"connections">) => Effect.Effect<void, Error>;
};
/**
* DIMENSION 5: Events
* All actions - created, updated, purchased, completed
*/
events: {
/**
* Get event by ID
* @returns Effect with Event or EventNotFoundError
*/
get: (id: Id<"events">) => Effect.Effect<Event, EventNotFoundError>;
/**
* List events with filters
* @returns Effect with array of events
*/
list: (params: ListEventsParams) => Effect.Effect<Event[], Error>;
/**
* Log new event
* @returns Effect with event ID
*/
log: (input: LogEventInput) => Effect.Effect<Id<"events">, Error>;
/**
* Get event timeline for thing
* @returns Effect with array of events
*/
getTimeline: (thingId: Id<"things">) => Effect.Effect<Event[], Error>;
/**
* Get event statistics
* @returns Effect with event stats
*/
getStats: (params: EventStatsParams) => Effect.Effect<EventStats, Error>;
};
/**
* DIMENSION 6: Knowledge
* Labels, embeddings, semantic search
*/
knowledge: {
/**
* Get knowledge item by ID
* @returns Effect with Knowledge or KnowledgeNotFoundError
*/
get: (
id: Id<"knowledge">,
) => Effect.Effect<Knowledge, KnowledgeNotFoundError>;
/**
* List knowledge items
* @returns Effect with array of knowledge items
*/
list: (params: ListKnowledgeParams) => Effect.Effect<Knowledge[], Error>;
/**
* Create knowledge item (label or chunk)
* @returns Effect with knowledge ID
*/
create: (
input: CreateKnowledgeInput,
) => Effect.Effect<Id<"knowledge">, Error>;
/**
* Link knowledge to thing
* @returns Effect with void
*/
link: (input: LinkKnowledgeInput) => Effect.Effect<void, Error>;
/**
* Unlink knowledge from thing
* @returns Effect with void
*/
unlink: (
knowledgeId: Id<"knowledge">,
thingId: Id<"things">,
) => Effect.Effect<void, Error>;
/**
* Vector search (semantic search)
* @returns Effect with search results
*/
vectorSearch: (
params: VectorSearchParams,
) => Effect.Effect<Knowledge[], Error>;
};
}
/**
* Type definitions for DataProvider operations
*/
// Organizations
export interface Organization {
_id: Id<"organizations">;
name: string;
slug: string;
plan: "starter" | "pro" | "enterprise";
status: "active" | "suspended" | "trial" | "cancelled";
limits: {
users: number;
storage: number;
apiCalls: number;
cycle: number;
};
usage: {
users: number;
storage: number;
apiCalls: number;
cycle: number;
};
createdAt: number;
updatedAt: number;
}
export interface ListOrganizationsParams {
status?: Organization["status"];
plan?: Organization["plan"];
limit?: number;
offset?: number;
}
export interface CreateOrganizationInput {
name: string;
slug: string;
plan: Organization["plan"];
ownerId: Id<"people">;
}
export interface OrganizationUsage {
organizationId: Id<"organizations">;
users: number;
storage: number;
apiCalls: number;
cycle: number;
period: { start: number; end: number };
}
// People
export interface Person {
_id: Id<"people">;
email: string;
username: string;
displayName: string;
role: "platform_owner" | "org_owner" | "org_user" | "customer";
organizationId?: Id<"organizations">;
permissions?: string[];
createdAt: number;
updatedAt: number;
}
export interface ListPeopleParams {
organizationId?: Id<"organizations">;
role?: Person["role"];
limit?: number;
offset?: number;
}
export interface CreatePersonInput {
email: string;
username: string;
displayName: string;
role: Person["role"];
organizationId?: Id<"organizations">;
permissions?: string[];
}
// Things
export interface Thing {
_id: Id<"things">;
type: ThingType;
name: string;
properties: Record<string, any>;
status: "active" | "inactive" | "draft" | "published" | "archived";
organizationId?: Id<"organizations">;
createdAt: number;
updatedAt: number;
}
export type ThingType =
| "creator"
| "ai_clone"
| "course"
| "lesson"
| "token"
| "external_agent"
| "mandate"
| "product";
export interface ListThingsParams {
type?: ThingType;
status?: Thing["status"];
organizationId?: Id<"organizations">;
limit?: number;
offset?: number;
}
export interface CreateThingInput {
type: ThingType;
name: string;
properties: Record<string, any>;
status?: Thing["status"];
organizationId?: Id<"organizations">;
}
export interface SearchThingsParams {
query: string;
type?: ThingType;
organizationId?: Id<"organizations">;
limit?: number;
}
// Connections
export interface Connection {
_id: Id<"connections">;
fromThingId: Id<"things">;
toThingId: Id<"things">;
relationshipType: ConnectionType;
metadata?: Record<string, any>;
validFrom?: number;
validTo?: number;
createdAt: number;
}
export type ConnectionType =
| "owns"
| "created_by"
| "clone_of"
| "holds_tokens"
| "enrolled_in"
| "transacted";
export interface ListConnectionsFromParams {
fromThingId: Id<"things">;
relationshipType?: ConnectionType;
limit?: number;
offset?: number;
}
export interface ListConnectionsToParams {
toThingId: Id<"things">;
relationshipType?: ConnectionType;
limit?: number;
offset?: number;
}
export interface CreateConnectionInput {
fromThingId: Id<"things">;
toThingId: Id<"things">;
relationshipType: ConnectionType;
metadata?: Record<string, any>;
validFrom?: number;
validTo?: number;
}
// Events
export interface Event {
_id: Id<"events">;
type: EventType;
actorId?: Id<"things">;
targetId?: Id<"things">;
timestamp: number;
metadata?: Record<string, any>;
}
export type EventType =
| "entity_created"
| "entity_updated"
| "entity_deleted"
| "payment_event"
| "communication_event";
export interface ListEventsParams {
type?: EventType;
actorId?: Id<"things">;
targetId?: Id<"things">;
startTime?: number;
endTime?: number;
limit?: number;
offset?: number;
}
export interface LogEventInput {
type: EventType;
actorId?: Id<"things">;
targetId?: Id<"things">;
metadata?: Record<string, any>;
}
export interface EventStatsParams {
type?: EventType;
organizationId?: Id<"organizations">;
period: { start: number; end: number };
}
export interface EventStats {
total: number;
byType: Record<EventType, number>;
byDay: Array<{ date: string; count: number }>;
}
// Knowledge
export interface Knowledge {
_id: Id<"knowledge">;
knowledgeType: "label" | "chunk" | "document" | "vector_only";
text?: string;
embedding?: number[];
embeddingModel?: string;
embeddingDim?: number;
sourceThingId?: Id<"things">;
labels?: string[];
createdAt: number;
}
export interface ListKnowledgeParams {
knowledgeType?: Knowledge["knowledgeType"];
sourceThingId?: Id<"things">;
limit?: number;
offset?: number;
}
export interface CreateKnowledgeInput {
knowledgeType: Knowledge["knowledgeType"];
text?: string;
embedding?: number[];
embeddingModel?: string;
sourceThingId?: Id<"things">;
labels?: string[];
}
export interface LinkKnowledgeInput {
knowledgeId: Id<"knowledge">;
thingId: Id<"things">;
role?: "label" | "summary" | "chunk_of" | "caption";
metadata?: Record<string, any>;
}
export interface VectorSearchParams {
query: string;
organizationId?: Id<"organizations">;
limit?: number;
}
/**
* Error types for DataProvider operations
*/
export class OrganizationNotFoundError {
readonly _tag = "OrganizationNotFoundError";
constructor(public readonly id: Id<"organizations">) {}
}
export class PersonNotFoundError {
readonly _tag = "PersonNotFoundError";
constructor(public readonly id: Id<"people">) {}
}
export class ThingNotFoundError {
readonly _tag = "ThingNotFoundError";
constructor(public readonly id: Id<"things">) {}
}
export class ConnectionNotFoundError {
readonly _tag = "ConnectionNotFoundError";
constructor(public readonly id: Id<"connections">) {}
}
export class EventNotFoundError {
readonly _tag = "EventNotFoundError";
constructor(public readonly id: Id<"events">) {}
}
export class KnowledgeNotFoundError {
readonly _tag = "KnowledgeNotFoundError";
constructor(public readonly id: Id<"knowledge">) {}
}
export class UnauthorizedError {
readonly _tag = "UnauthorizedError";
constructor(public readonly message: string) {}
}
export class ValidationError {
readonly _tag = "ValidationError";
constructor(public readonly message: string) {}
}
```
### 1.2 ConvexProvider Implementation
Complete implementation wrapping Convex SDK:
```typescript
// frontend/src/providers/ConvexProvider.ts
import { Effect } from "effect";
import { ConvexReactClient } from "convex/react";
import { api } from "@/convex/_generated/api";
import type { DataProvider } from "./DataProvider";
import * as Errors from "./DataProvider";
/**
* ConvexProvider - Concrete implementation of DataProvider using Convex SDK
*
* This provider:
* 1. Wraps ALL Convex operations in Effect.ts
* 2. Preserves authentication context
* 3. Converts Convex errors to typed errors
* 4. Ensures organization scoping
* 5. Maintains performance (<10ms overhead)
*
* Design Principles:
* - Thin wrapper (no business logic)
* - Direct mapping to Convex queries/mutations
* - Error transformation only
* - Type-safe at compile time
*/
export class ConvexProvider implements DataProvider {
constructor(private convex: ConvexReactClient) {}
organizations = {
get: (id: Id<"organizations">) =>
Effect.tryPromise({
try: () => this.convex.query(api.queries.organizations.get, { id }),
catch: () => new Errors.OrganizationNotFoundError(id),
}),
list: (params: ListOrganizationsParams) =>
Effect.tryPromise({
try: () => this.convex.query(api.queries.organizations.list, params),
catch: (error) => new Error(String(error)),
}),
create: (input: CreateOrganizationInput) =>
Effect.tryPromise({
try: () =>
this.convex.mutation(api.mutations.organizations.create, input),
catch: (error) => new Error(String(error)),
}),
update: (id: Id<"organizations">, updates: Partial<Organization>) =>
Effect.tryPromise({
try: () =>
this.convex.mutation(api.mutations.organizations.update, {
id,
...updates,
}),
catch: (error) => new Error(String(error)),
}),
getUsage: (id: Id<"organizations">) =>
Effect.tryPromise({
try: () =>
this.convex.query(api.queries.organizations.getUsage, { id }),
catch: (error) => new Error(String(error)),
}),
};
people = {
get: (id: Id<"people">) =>
Effect.tryPromise({
try: () => this.convex.query(api.queries.people.get, { id }),
catch: () => new Errors.PersonNotFoundError(id),
}),
list: (params: ListPeopleParams) =>
Effect.tryPromise({
try: () => this.convex.query(api.queries.people.list, params),
catch: (error) => new Error(String(error)),
}),
create: (input: CreatePersonInput) =>
Effect.tryPromise({
try: () => this.convex.mutation(api.mutations.people.create, input),
catch: (error) => new Error(String(error)),
}),
update: (id: Id<"people">, updates: Partial<Person>) =>
Effect.tryPromise({
try: () =>
this.convex.mutation(api.mutations.people.update, { id, ...updates }),
catch: (error) => new Error(String(error)),
}),
getOrganizations: (id: Id<"people">) =>
Effect.tryPromise({
try: () =>
this.convex.query(api.queries.people.getOrganizations, { id }),
catch: (error) => new Error(String(error)),
}),
checkPermission: (id: Id<"people">, permission: string) =>
Effect.tryPromise({
try: () =>
this.convex.query(api.queries.people.checkPermission, {
id,
permission,
}),
catch: (error) => new Error(String(error)),
}),
};
things = {
get: (id: Id<"things">) =>
Effect.tryPromise({
try: () => this.convex.query(api.queries.things.get, { id }),
catch: () => new Errors.ThingNotFoundError(id),
}),
list: (params: ListThingsParams) =>
Effect.tryPromise({
try: () => this.convex.query(api.queries.things.list, params),
catch: (error) => new Error(String(error)),
}),
create: (input: CreateThingInput) =>
Effect.tryPromise({
try: () => this.convex.mutation(api.mutations.things.create, input),
catch: (error) => new Error(String(error)),
}),
update: (id: Id<"things">, updates: Partial<Thing>) =>
Effect.tryPromise({
try: () =>
this.convex.mutation(api.mutations.things.update, { id, ...updates }),
catch: (error) => new Error(String(error)),
}),
delete: (id: Id<"things">) =>
Effect.tryPromise({
try: () => this.convex.mutation(api.mutations.things.delete, { id }),
catch: (error) => new Error(String(error)),
}),
search: (params: SearchThingsParams) =>
Effect.tryPromise({
try: () => this.convex.query(api.queries.things.search, params),
catch: (error) => new Error(String(error)),
}),
};
connections = {
get: (id: Id<"connections">) =>
Effect.tryPromise({
try: () => this.convex.query(api.queries.connections.get, { id }),
catch: () => new Errors.ConnectionNotFoundError(id),
}),
listFrom: (params: ListConnectionsFromParams) =>
Effect.tryPromise({
try: () => this.convex.query(api.queries.connections.listFrom, params),
catch: (error) => new Error(String(error)),
}),
listTo: (params: ListConnectionsToParams) =>
Effect.tryPromise({
try: () => this.convex.query(api.queries.connections.listTo, params),
catch: (error) => new Error(String(error)),
}),
create: (input: CreateConnectionInput) =>
Effect.tryPromise({
try: () =>
this.convex.mutation(api.mutations.connections.create, input),
catch: (error) => new Error(String(error)),
}),
update: (id: Id<"connections">, updates: Partial<Connection>) =>
Effect.tryPromise({
try: () =>
this.convex.mutation(api.mutations.connections.update, {
id,
...updates,
}),
catch: (error) => new Error(String(error)),
}),
delete: (id: Id<"connections">) =>
Effect.tryPromise({
try: () =>
this.convex.mutation(api.mutations.connections.delete, { id }),
catch: (error) => new Error(String(error)),
}),
};
events = {
get: (id: Id<"events">) =>
Effect.tryPromise({
try: () => this.convex.query(api.queries.events.get, { id }),
catch: () => new Errors.EventNotFoundError(id),
}),
list: (params: ListEventsParams) =>
Effect.tryPromise({
try: () => this.convex.query(api.queries.events.list, params),
catch: (error) => new Error(String(error)),
}),
log: (input: LogEventInput) =>
Effect.tryPromise({
try: () => this.convex.mutation(api.mutations.events.log, input),
catch: (error) => new Error(String(error)),
}),
getTimeline: (thingId: Id<"things">) =>
Effect.tryPromise({
try: () =>
this.convex.query(api.queries.events.getTimeline, { thingId }),
catch: (error) => new Error(String(error)),
}),
getStats: (params: EventStatsParams) =>
Effect.tryPromise({
try: () => this.convex.query(api.queries.events.getStats, params),
catch: (error) => new Error(String(error)),
}),
};
knowledge = {
get: (id: Id<"knowledge">) =>
Effect.tryPromise({
try: () => this.convex.query(api.queries.knowledge.get, { id }),
catch: () => new Errors.KnowledgeNotFoundError(id),
}),
list: (params: ListKnowledgeParams) =>
Effect.tryPromise({
try: () => this.convex.query(api.queries.knowledge.list, params),
catch: (error) => new Error(String(error)),
}),
create: (input: CreateKnowledgeInput) =>
Effect.tryPromise({
try: () => this.convex.mutation(api.mutations.knowledge.create, input),
catch: (error) => new Error(String(error)),
}),
link: (input: LinkKnowledgeInput) =>
Effect.tryPromise({
try: () => this.convex.mutation(api.mutations.knowledge.link, input),
catch: (error) => new Error(String(error)),
}),
unlink: (knowledgeId: Id<"knowledge">, thingId: Id<"things">) =>
Effect.tryPromise({
try: () =>
this.convex.mutation(api.mutations.knowledge.unlink, {
knowledgeId,
thingId,
}),
catch: (error) => new Error(String(error)),
}),
vectorSearch: (params: VectorSearchParams) =>
Effect.tryPromise({
try: () =>
this.convex.query(api.queries.knowledge.vectorSearch, params),
catch: (error) => new Error(String(error)),
}),
};
}
/**
* Factory function to create ConvexProvider
*/
export const createConvexProvider = (
convex: ConvexReactClient,
): DataProvider => {
return new ConvexProvider(convex);
};
/**
* React hook for using DataProvider
*/
import { useConvex } from "convex/react";
import { useMemo } from "react";
export const useDataProvider = (): DataProvider => {
const convex = useConvex();
return useMemo(() => createConvexProvider(convex), [convex]);
};
```
### 1.3 Effect.ts Layer Creation
```typescript
// frontend/src/providers/layers.ts
import { Layer, Context } from "effect";
import type { DataProvider } from "./DataProvider";
/**
* DataProvider Context - Effect.ts service definition
*/
export class DataProviderService extends Context.Tag("DataProviderService")<
DataProviderService,
DataProvider
>() {}
/**
* Create Layer from concrete DataProvider instance
*/
export const DataProviderLayer = (
provider: DataProvider,
): Layer.Layer<DataProviderService> =>
Layer.succeed(DataProviderService, provider);
/**
* Example usage in Effect program:
*
* const program = Effect.gen(function* () {
* const provider = yield* DataProviderService;
* const thing = yield* provider.things.get(thingId);
* return thing;
* });
*
* const result = await Effect.runPromise(
* program.pipe(Effect.provide(DataProviderLayer(convexProvider)))
* );
*/
```
---
## 2. Ontology Mapping
### Organizations (Dimension 1)
**Role:** Multi-tenant isolation and backend provider configuration
- **Thing Type:** `external_connection` (type for provider configs)
- **Properties:**
```typescript
{
platform: "convex" | "supabase" | "firebase" | "custom",
name: "Production Backend",
baseUrl: "https://shocking-falcon-870.convex.cloud",
connectionType: "rest" | "websocket" | "graphql",
status: "active" | "inactive" | "error",
organizationId: Id<"organizations">
}
```
- **Organization Scoping:** EVERY DataProvider operation MUST filter by `organizationId`
- **Backend Selection:** Organizations can configure which backend provider to use (stored in `organizations.properties.backendProvider`)
### People (Dimension 2)
**Role:** Authorization - who can configure backend providers
- **Permissions:** Only `platform_owner` and `org_owner` can configure backend providers
- **Actions:**
- `provider:configure` - Change backend provider
- `provider:view` - View provider configuration
- `provider:test` - Test provider connection
- **Audit:** ALL provider configuration changes MUST log events with actorId
### Things (Dimension 3)
**Role:** Provider configurations stored as things
- **Thing Type:** `external_connection`
- **Created When:** Organization selects/configures backend provider
- **Properties:** Provider-specific configuration (baseUrl, apiKey, etc.)
- **Status:** `active` (current provider) or `inactive` (past providers)
- **Relationships:**
- Organization → external_connection (`owns`)
- Organization → external_connection (`configured_by`)
### Connections (Dimension 4)
**Role:** Link organizations to their backend provider configuration
- **Connection Type:** `configured_by`
- **Pattern:**
```typescript
{
fromThingId: organizationId,
toThingId: externalConnectionId,
relationshipType: "configured_by",
metadata: {
configuredAt: Date.now(),
configuredBy: personId,
previousProvider?: "convex" | "supabase"
}
}
```
### Events (Dimension 5)
**Role:** Log all provider configuration changes
- **Event Types:**
- `provider_configured` - Initial provider setup
- `provider_changed` - Switch from one provider to another
- `provider_tested` - Connection test performed
- `provider_error` - Provider connection failed
- **Pattern:**
```typescript
{
type: "provider_changed",
actorId: personId,
targetId: organizationId,
timestamp: Date.now(),
metadata: {
oldProvider: "convex",
newProvider: "supabase",
reason: "performance"
}
}
```
### Knowledge (Dimension 6)
**Role:** Labels and tags for provider capabilities
- **Labels:**
- `capability:realtime` - Real-time subscriptions
- `capability:vector_search` - Vector search support
- `capability:file_storage` - File upload/storage
- `protocol:rest` - REST API support
- `protocol:websocket` - WebSocket support
- `protocol:graphql` - GraphQL API support
- **Usage:** Filter available providers by required capabilities
---
## 3. User Stories
### Story 1: Developer Uses Interface
**As a** frontend developer
**I want** to use a single `DataProvider` interface for all backend operations
**So that** I don't need to learn Convex-specific APIs
**Acceptance Criteria:**
1. Can import `DataProvider` interface from `@/providers/DataProvider`
2. All operations return `Effect.Effect<T, Error>` (typed errors)
3. No Convex imports needed in components (only `useDataProvider` hook)
4. TypeScript autocomplete shows all available operations
5. Documentation includes examples for each operation
### Story 2: Backend Developer Wraps Convex
**As a** backend developer
**I want** to implement `ConvexProvider` that wraps Convex SDK
**So that** frontend can use Convex without coupling
**Acceptance Criteria:**
1. `ConvexProvider` implements ALL methods in `DataProvider` interface
2. Each method wraps corresponding Convex query/mutation
3. Convex errors are converted to typed errors (`ThingNotFoundError`, etc.)
4. Authentication context is preserved through all operations
5. Performance overhead is <10ms per operation
### Story 3: Developer Tests in Isolation
**As a** developer
**I want** to mock `DataProvider` in tests
**So that** I can test components without real backend
**Acceptance Criteria:**
1. Can create mock implementation of `DataProvider`
2. Mock returns `Effect.succeed()` or `Effect.fail()` as needed
3. Tests run without Convex connection
4. Tests run in <1 second total
5. Test coverage >90% for all provider operations
### Story 4: Organization Configures Provider
**As an** organization owner
**I want** to configure which backend provider my organization uses
**So that** I can choose the best solution for my needs
**Acceptance Criteria:**
1. Can view current backend provider in organization settings
2. Can switch to different provider (Convex → Supabase)
3. Provider change is logged as event with actorId
4. Old provider configuration is archived (not deleted)
5. Application continues working after provider switch
### Story 5: Developer Uses Effect.ts
**As a** developer
**I want** to compose DataProvider operations using Effect.ts
**So that** I can build complex workflows with proper error handling
**Acceptance Criteria:**
1. All operations return `Effect.Effect<T, Error>`
2. Can use `Effect.gen()` to compose operations
3. Can use `Effect.catchAll()` to handle errors
4. Can use `Effect.provide()` to inject provider
5. TypeScript infers error types correctly
---
## 4. Implementation Steps
### Phase 1: Interface Definition (Days 1-2)
**Step 1:** Create DataProvider interface file
```bash
mkdir -p frontend/src/providers
touch frontend/src/providers/DataProvider.ts
```
**Step 2:** Define base DataProvider interface with all 6 dimensions
```typescript
// Copy complete interface from section 1.1
```
**Step 3:** Define all type definitions (Organization, Person, Thing, etc.)
```typescript
// Copy all type definitions from section 1.1
```
**Step 4:** Define error classes with `_tag` pattern
```typescript
// Copy all error classes from section 1.1
```
**Step 5:** Export all types and interfaces
```typescript
export type {
DataProvider,
Organization,
Person,
Thing,
Connection,
Event,
Knowledge,
};
export {
OrganizationNotFoundError,
PersonNotFoundError,
ThingNotFoundError /* etc */,
};
```
**Step 6:** Validate TypeScript compiles
```bash
cd frontend && bunx astro check
```
### Phase 2: ConvexProvider Implementation (Days 3-4)
**Step 7:** Create ConvexProvider implementation file
```bash
touch frontend/src/providers/ConvexProvider.ts
```
**Step 8:** Implement ConvexProvider class
```typescript
// Copy complete implementation from section 1.2
```
**Step 9:** Implement factory function `createConvexProvider`
```typescript
// Copy factory function from section 1.2
```
**Step 10:** Implement React hook `useDataProvider`
```typescript
// Copy React hook from section 1.2
```
**Step 11:** Validate ConvexProvider implements DataProvider
```bash
bunx astro check
```
**Step 12:** Test ConvexProvider methods manually
```bash
bun run dev
# Visit http://localhost:4321/test-provider
```
### Phase 3: Effect.ts Integration (Day 5)
**Step 13:** Create Layer definitions
```bash
touch frontend/src/providers/layers.ts
```
**Step 14:** Define DataProviderService context
```typescript
// Copy from section 1.3
```
**Step 15:** Create Layer factory function
```typescript
// Copy from section 1.3
```
**Step 16:** Test Effect.ts composition
```typescript
// Create test program
const program = Effect.gen(function* () {
const provider = yield* DataProviderService;
const things = yield* provider.things.list({ type: "course" });
return things;
});
const result = await Effect.runPromise(
program.pipe(Effect.provide(DataProviderLayer(convexProvider))),
);
```
**Step 17:** Validate all Effect operations compile
```bash
bunx astro check
```
### Phase 4: Backend Queries Implementation (Days 6-7)
**Step 18:** Create backend query files
```bash
mkdir -p backend/convex/queries
touch backend/convex/queries/organizations.ts
touch backend/convex/queries/people.ts
touch backend/convex/queries/things.ts
touch backend/convex/queries/connections.ts
touch backend/convex/queries/events.ts
touch backend/convex/queries/knowledge.ts
```
**Step 19:** Implement organizations queries
```typescript
// backend/convex/queries/organizations.ts
import { query } from "../_generated/server";
import { v } from "convex/values";
export const get = query({
args: { id: v.id("organizations") },
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
},
});
export const list = query({
args: {
status: v.optional(v.string()),
plan: v.optional(v.string()),
limit: v.optional(v.number()),
offset: v.optional(v.number()),
},
handler: async (ctx, args) => {
let q = ctx.db.query("organizations");
if (args.status) {
q = q.filter((q) => q.eq(q.field("status"), args.status));
}
return await q.take(args.limit || 100);
},
});
export const getUsage = query({
args: { id: v.id("organizations") },
handler: async (ctx, args) => {
const org = await ctx.db.get(args.id);
if (!org) throw new Error("Organization not found");
return {
organizationId: args.id,
users: org.usage.users,
storage: org.usage.storage,
apiCalls: org.usage.apiCalls,
cycle: org.usage.cycle,
period: { start: Date.now() - 30 * 24 * 60 * 60 * 1000, end: Date.now() },
};
},
});
```
**Step 20:** Implement people queries (similar pattern)
```typescript
// backend/convex/queries/people.ts
// Follow same pattern as organizations
```
**Step 21:** Implement things queries (with search)
```typescript
// backend/convex/queries/things.ts
import { query } from "../_generated/server";
import { v } from "convex/values";
export const get = query({
args: { id: v.id("things") },
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
},
});
export const list = query({
args: {
type: v.optional(v.string()),
status: v.optional(v.string()),
organizationId: v.optional(v.id("organizations")),
limit: v.optional(v.number()),
offset: v.optional(v.number()),
},
handler: async (ctx, args) => {
let q = ctx.db.query("things");
if (args.type) {
q = q.withIndex("by_type", (q) => q.eq("type", args.type));
}
if (args.organizationId) {
q = q.filter((q) => q.eq(q.field("organizationId"), args.organizationId));
}
if (args.status) {
q = q.filter((q) => q.eq(q.field("status"), args.status));
}
return await q.take(args.limit || 100);
},
});
export const search = query({
args: {
query: v.string(),
type: v.optional(v.string()),
organizationId: v.optional(v.id("organizations")),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
return await ctx.db
.query("things")
.withSearchIndex("search_things", (q) => q.search("name", args.query))
.filter((q) => {
if (args.type) return q.eq(q.field("type"), args.type);
return true;
})
.take(args.limit || 20);
},
});
```
**Step 22:** Implement connections queries
```typescript
// backend/convex/queries/connections.ts
// Implement listFrom and listTo with indexes
```
**Step 23:** Implement events queries
```typescript
// backend/convex/queries/events.ts
// Implement list, getTimeline, getStats
```
**Step 24:** Implement knowledge queries
```typescript
// backend/convex/queries/knowledge.ts
// Implement list, vectorSearch
```
**Step 25:** Test all queries from frontend
```bash
cd frontend && bun run dev
# Test each query type
```
### Phase 5: Backend Mutations Implementation (Days 8-9)
**Step 26:** Create backend mutation files
```bash
mkdir -p backend/convex/mutations
touch backend/convex/mutations/organizations.ts
touch backend/convex/mutations/people.ts
touch backend/convex/mutations/things.ts
touch backend/convex/mutations/connections.ts
touch backend/convex/mutations/events.ts
touch backend/convex/mutations/knowledge.ts
```
**Step 27:** Implement organizations mutations
```typescript
// backend/convex/mutations/organizations.ts
import { mutation } from "../_generated/server";
import { v } from "convex/values";
export const create = mutation({
args: {
name: v.string(),
slug: v.string(),
plan: v.string(),
ownerId: v.id("people"),
},
handler: async (ctx, args) => {
const orgId = await ctx.db.insert("organizations", {
name: args.name,
slug: args.slug,
plan: args.plan as any,
status: "trial",
limits: { users: 5, storage: 1, apiCalls: 1000, cycle: 100 },
usage: { users: 1, storage: 0, apiCalls: 0, cycle: 0 },
createdAt: Date.now(),
updatedAt: Date.now(),
});
// Log event
await ctx.db.insert("events", {
type: "organization_created",
actorId: args.ownerId,
targetId: orgId,
timestamp: Date.now(),
metadata: { plan: args.plan },
});
return orgId;
},
});
export const update = mutation({
args: {
id: v.id("organizations"),
name: v.optional(v.string()),
plan: v.optional(v.string()),
status: v.optional(v.string()),
},
handler: async (ctx, args) => {
const { id, ...updates } = args;
await ctx.db.patch(id, { ...updates, updatedAt: Date.now() });
const updated = await ctx.db.get(id);
return updated;
},
});
```
**Step 28:** Implement people mutations
```typescript
// backend/convex/mutations/people.ts
// Follow same pattern
```
**Step 29:** Implement things mutations (create, update, delete)
```typescript
// backend/convex/mutations/things.ts
import { mutation } from "../_generated/server";
import { v } from "convex/values";
export const create = mutation({
args: {
type: v.string(),
name: v.string(),
properties: v.any(),
status: v.optional(v.string()),
organizationId: v.optional(v.id("organizations")),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
const thingId = await ctx.db.insert("things", {
type: args.type as any,
name: args.name,
properties: args.properties,
status: (args.status as any) || "draft",
organizationId: args.organizationId,
createdAt: Date.now(),
updatedAt: Date.now(),
});
// Log event
await ctx.db.insert("events", {
type: "entity_created",
actorId: identity.tokenIdentifier,
targetId: thingId,
timestamp: Date.now(),
metadata: { entityType: args.type },
});
return thingId;
},
});
export const update = mutation({
args: {
id: v.id("things"),
name: v.optional(v.string()),
properties: v.optional(v.any()),
status: v.optional(v.string()),
},
handler: async (ctx, args) => {
const { id, ...updates } = args;
await ctx.db.patch(id, { ...updates, updatedAt: Date.now() });
const updated = await ctx.db.get(id);
return updated;
},
});
export const delete = mutation({
args: { id: v.id("things") },
handler: async (ctx, args) => {
await ctx.db.patch(args.id, { deletedAt: Date.now() });
},
});
```
**Step 30:** Implement connections mutations
```typescript
// backend/convex/mutations/connections.ts
// Implement create, update, delete
```
**Step 31:** Implement events mutations
```typescript
// backend/convex/mutations/events.ts
export const log = mutation({
args: {
type: v.string(),
actorId: v.optional(v.id("things")),
targetId: v.optional(v.id("things")),
metadata: v.optional(v.any()),
},
handler: async (ctx, args) => {
return await ctx.db.insert("events", {
type: args.type as any,
actorId: args.actorId,
targetId: args.targetId,
timestamp: Date.now(),
metadata: args.metadata,
});
},
});
```
**Step 32:** Implement knowledge mutations
```typescript
// backend/convex/mutations/knowledge.ts
// Implement create, link, unlink
```
**Step 33:** Test all mutations from frontend
```bash
cd frontend && bun run dev
# Test each mutation type
```
### Phase 6: Testing (Day 10)
**Step 34:** Create test directory
```bash
mkdir -p frontend/test/providers
touch frontend/test/providers/DataProvider.test.ts
touch frontend/test/providers/ConvexProvider.test.ts
```
**Step 35:** Write DataProvider interface tests
```typescript
// frontend/test/providers/DataProvider.test.ts
import { describe, it, expect } from "vitest";
import type { DataProvider } from "@/providers/DataProvider";
describe("DataProvider Interface", () => {
it("should define all 6 dimensions", () => {
const operations: Array<keyof DataProvider> = [
"organizations",
"people",
"things",
"connections",
"events",
"knowledge",
];
// Type-level test: this compiles if interface is complete
expect(operations.length).toBe(6);
});
});
```
**Step 36:** Write ConvexProvider unit tests
```typescript
// frontend/test/providers/ConvexProvider.test.ts
import { describe, it, expect, vi } from "vitest";
import { Effect } from "effect";
import { ConvexProvider } from "@/providers/ConvexProvider";
import type { ConvexReactClient } from "convex/react";
describe("ConvexProvider", () => {
it("should get organization by id", async () => {
const mockConvex = {
query: vi.fn().mockResolvedValue({
_id: "org_123",
name: "Test Org",
plan: "pro",
}),
} as unknown as ConvexReactClient;
const provider = new ConvexProvider(mockConvex);
const result = await Effect.runPromise(
provider.organizations.get("org_123" as any),
);
expect(result.name).toBe("Test Org");
});
it("should handle not found errors", async () => {
const mockConvex = {
query: vi.fn().mockRejectedValue(new Error("Not found")),
} as unknown as ConvexReactClient;
const provider = new ConvexProvider(mockConvex);
const result = Effect.runPromiseExit(
provider.organizations.get("org_123" as any),
);
await expect(result).rejects.toMatchObject({
_tag: "OrganizationNotFoundError",
});
});
});
```
**Step 37:** Write integration tests
```typescript
// frontend/test/providers/integration.test.ts
import { describe, it, expect } from "vitest";
import { ConvexHttpClient } from "convex/browser";
import { createConvexProvider } from "@/providers/ConvexProvider";
import { Effect } from "effect";
describe("ConvexProvider Integration", () => {
const convex = new ConvexHttpClient(import.meta.env.PUBLIC_CONVEX_URL);
const provider = createConvexProvider(convex);
it("should list things", async () => {
const result = await Effect.runPromise(
provider.things.list({ type: "course", limit: 10 }),
);
expect(Array.isArray(result)).toBe(true);
});
it("should create and delete thing", async () => {
const createResult = await Effect.runPromise(
provider.things.create({
type: "course",
name: "Test Course",
properties: { description: "Test" },
}),
);
expect(createResult).toBeDefined();
await Effect.runPromise(provider.things.delete(createResult));
});
});
```
**Step 38:** Run all tests
```bash
cd frontend && bun test test/providers
```
**Step 39:** Measure test coverage
```bash
bun test --coverage test/providers
# Target: >90% coverage
```
**Step 40:** Write performance tests
```typescript
// frontend/test/providers/performance.test.ts
import { describe, it, expect } from "vitest";
import { createConvexProvider } from "@/providers/ConvexProvider";
import { Effect } from "effect";
describe("ConvexProvider Performance", () => {
it("should have <10ms overhead per operation", async () => {
const provider = createConvexProvider(convex);
const start = performance.now();
await Effect.runPromise(
provider.things.list({ type: "course", limit: 100 }),
);
const end = performance.now();
const overhead = end - start;
expect(overhead).toBeLessThan(10);
});
});
```
---
## 5. Testing Strategy
### Unit Tests (90%+ coverage)
**Test 1: DataProvider Interface Type Checking**
```typescript
// Ensures interface is complete
import type { DataProvider } from "@/providers/DataProvider";
const validateInterface = (provider: DataProvider) => {
// TypeScript enforces all methods exist
expect(provider.organizations).toBeDefined();
expect(provider.people).toBeDefined();
expect(provider.things).toBeDefined();
expect(provider.connections).toBeDefined();
expect(provider.events).toBeDefined();
expect(provider.knowledge).toBeDefined();
};
```
**Test 2: ConvexProvider Method Wrapping**
```typescript
describe("ConvexProvider.things.get", () => {
it("should call convex.query with correct args", async () => {
const mockQuery = vi.fn().mockResolvedValue({ _id: "thing_123" });
const mockConvex = { query: mockQuery } as any;
const provider = new ConvexProvider(mockConvex);
await Effect.runPromise(provider.things.get("thing_123" as any));
expect(mockQuery).toHaveBeenCalledWith(
expect.any(Function), // api.queries.things.get
{ id: "thing_123" },
);
});
});
```
**Test 3: Error Transformation**
```typescript
describe("ConvexProvider Error Handling", () => {
it("should transform 404 to ThingNotFoundError", async () => {
const mockConvex = {
query: vi.fn().mockRejectedValue(new Error("Not found")),
} as any;
const provider = new ConvexProvider(mockConvex);
const result = await Effect.runPromiseExit(
provider.things.get("thing_123" as any),
);
expect(result._tag).toBe("Failure");
expect(result.cause._tag).toBe("ThingNotFoundError");
});
});
```
**Test 4: Effect.ts Composition**
```typescript
describe("Effect.ts Composition", () => {
it("should compose multiple operations", async () => {
const program = Effect.gen(function* () {
const provider = yield* DataProviderService;
const thing = yield* provider.things.get("thing_123" as any);
const connections = yield* provider.connections.listFrom({
fromThingId: thing._id,
});
return { thing, connections };
});
const result = await Effect.runPromise(
program.pipe(Effect.provide(DataProviderLayer(mockProvider))),
);
expect(result.thing).toBeDefined();
expect(result.connections).toBeDefined();
});
});
```
**Test 5: All 6 Dimensions Covered**
```typescript
describe("Full DataProvider Coverage", () => {
const dimensions = [
"organizations",
"people",
"things",
"connections",
"events",
"knowledge",
] as const;
dimensions.forEach((dimension) => {
it(`should implement ${dimension} operations`, () => {
const provider = createConvexProvider(mockConvex);
expect(provider[dimension]).toBeDefined();
expect(typeof provider[dimension]).toBe("object");
});
});
});
```
### Integration Tests (80%+ coverage)
**Test 6: Create Thing Flow**
```typescript
describe("Create Thing Integration", () => {
it("should create thing, log event, return ID", async () => {
const provider = createConvexProvider(convex);
const thingId = await Effect.runPromise(
provider.things.create({
type: "course",
name: "Integration Test Course",
properties: { description: "Test" },
}),
);
expect(thingId).toMatch(/^[a-z0-9]+$/);
// Verify thing was created
const thing = await Effect.runPromise(provider.things.get(thingId));
expect(thing.name).toBe("Integration Test Course");
// Verify event was logged
const events = await Effect.runPromise(
provider.events.list({ targetId: thingId }),
);
expect(events.some((e) => e.type === "entity_created")).toBe(true);
// Cleanup
await Effect.runPromise(provider.things.delete(thingId));
});
});
```
**Test 7: Connection Creation Flow**
```typescript
describe("Create Connection Integration", () => {
it("should create connection between two things", async () => {
const provider = createCo