UNPKG

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,766 lines (1,398 loc) 63.4 kB
--- title: Separate dimension: things category: plans tags: architecture, backend, frontend, groups, ontology related_dimensions: connections, 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 plans category. Location: one/things/plans/separate.md Purpose: Documents frontend-backend separation plan Related dimensions: connections, events, groups For AI agents: Read this to understand separate. --- # Frontend-Backend Separation Plan ## Executive Summary **Goal:** Transform the current tightly-coupled architecture into a fully headless, **backend-agnostic** architecture where: - **Frontend:** Pure Astro/React UI with DataProvider interface (no backend dependency) - **Backend:** ANY backend that implements the 6-dimension ontology (Convex, WordPress, Notion, Supabase, etc.) - **Connection:** DataProvider interface - swap backends by changing ONE line **Current State:** - ✅ Frontend is **working** and connected to Convex backend - ✅ Auth is functional with tests in `frontend/tests/auth/*` - ✅ Direct Convex integration (using `useQuery`, `useMutation`, Convex hooks) - ⚠️ Tightly coupled to Convex - can't swap to WordPress/Notion/Supabase **Target State:** - Frontend uses DataProvider interface - Backend is pluggable - groups can use Convex, their existing WordPress site, Notion databases, or any other backend - Existing auth tests continue to pass - All current functionality preserved while adding flexibility --- ## Important Context **This is NOT a bug fix - this is a strategic enhancement.** ✅ **Current System Works:** - Frontend successfully talks to Convex backend - Auth is functional with passing tests - All features operational - No critical issues requiring immediate changes ⚠️ **Why Separate Now:** - Add backend flexibility (support WordPress, Notion, Shopify, etc.) - Enable multi-backend federation (auth in Convex, blog in WordPress, products in Shopify) - Allow groups to use existing infrastructure - Remove Convex lock-in **Migration Approach:** - Zero downtime - wrap existing working functionality - Preserve all existing tests (especially auth tests) - Gradual page-by-page migration - Can rollback at any phase - No data migration required **Priority:** Medium (strategic, not urgent) --- ## Table of Contents 1. [Architecture Comparison](#architecture-comparison) 2. [Backend Options](#backend-options) 3. [Benefits of Separation](#benefits-of-separation) 4. [Migration Strategy](#migration-strategy) 5. [Backend Implementations](#backend-implementations) 6. [Testing Strategy](#testing-strategy) 7. [Deployment](#deployment) --- ## Architecture Comparison ### Current Architecture (Working, but Coupled) ``` ┌─────────────────────────────────────────────────────────┐ │ FRONTEND (Astro + React) │ │ ┌──────────────────────────────────────────────────┐ │ │ │ Pages & Components │ │ │ │ ├─ import { useQuery } from 'convex/react' │ │ │ │ ├─ import { api } from 'convex/_generated/api' │ │ │ │ └─ const data = useQuery(api.things.get) │ │ │ └──────────────────────────────────────────────────┘ │ │ │ │ frontend/convex/* (schema, mutations, queries) │ │ ↓ Direct WebSocket Connection (WORKING) │ │ │ │ frontend/tests/auth/* (Auth tests passing) │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────┐ │ CONVEX BACKEND │ │ (Real-time DB) │ │ 6-Dimension Ontology │ └───────────────────────┘ ``` **Status:** ✅ Working - Auth functional, tests passing **Limitations (Not Bugs):** - ⚠️ Frontend tightly coupled to Convex (works, but inflexible) - ⚠️ Can't swap backend without rewriting frontend - ⚠️ Groups must use Convex (can't use existing WordPress/Notion) - ⚠️ Hard to add mobile/desktop apps without Convex SDK - ⚠️ No multi-backend support (can't federate data from Shopify/WordPress) --- ### Target Architecture (Backend-Agnostic with 6-Dimension Ontology) ``` ┌──────────────────────────────────────────────────────────┐ │ FRONTEND (Backend-Agnostic) │ │ ┌────────────────────────────────────────────────────┐ │ │ │ Astro Pages + React Components │ │ │ │ - Renders UI from data │ │ │ │ - Calls DataProvider methods │ │ │ │ - NO backend-specific code │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ Effect.ts Services (Backend-Agnostic) │ │ │ │ - ThingService │ │ │ │ - ConnectionService │ │ │ │ - Uses DataProvider interface │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ DataProvider Interface (Universal API) │ │ │ │ groups: { get, list, update } │ │ │ │ people: { get, list, create, update } │ │ │ │ things: { get, list, create, update, delete } │ │ │ │ connections: { create, getRelated, getCount } │ │ │ │ events: { log, query } │ │ │ │ knowledge: { embed, search } │ │ │ └────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────┘ │ ↓ Provider Implementation ┌──────────────────────────────────────────────────────────┐ │ BACKEND PROVIDERS (Choose One - ONE Line!) │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Option 1: ConvexProvider │ │ │ │ → Convex backend (real-time, serverless) │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Option 2: WordPressProvider │ │ │ │ → WordPress + WooCommerce (existing CMS) │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Option 3: NotionProvider │ │ │ │ → Notion databases as backend │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Option 4: SupabaseProvider │ │ │ │ → PostgreSQL + real-time (pgvector) │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Option 5: CustomProvider │ │ │ │ → Your own API/database │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ All implement the same DataProvider interface │ └──────────────────────────────────────────────────────────┘ │ ↓ Backend-Specific Implementation ┌──────────────────────────────────────────────────────────┐ │ ACTUAL BACKENDS (Examples) │ │ │ │ Convex: │ │ 6 tables → groups, people, things, │ │ connections, events, knowledge │ │ │ │ WordPress: │ │ wp_posts, wp_users, wp_postmeta, wp_terms, etc. │ │ │ │ Notion: │ │ Databases, Pages, Relations │ │ │ │ Supabase: │ │ PostgreSQL tables with pgvector for knowledge │ │ │ │ Custom: │ │ Your own database schema │ └──────────────────────────────────────────────────────────┘ ``` **Benefits:** - ✅ **Backend-Agnostic:** Change backend by editing ONE line in config - ✅ **Multi-Backend Support:** Organizations can use existing infrastructure (WordPress, Notion, etc.) - ✅ **No Lock-In:** Not tied to Convex, any backend works - ✅ **Progressive Migration:** Keep existing WordPress site, add ONE frontend gradually - ✅ **Frontend Independence:** Rebuild UI without touching backend - ✅ **6-Dimension Ontology:** Universal data model works with any backend - ✅ **Effect.ts Services:** Type-safe, composable, testable operations - ✅ **Multi-Platform:** Same backend serves web, mobile, desktop, CLI - ✅ **Developer Choice:** Use the best backend for your organization's needs --- ## Understanding the 6-Dimension Ontology **The ONE ontology models reality in six core dimensions:** ``` ┌──────────────────────────────────────────────────────┐ │ 1. GROUPS → Who owns this space? │ │ Multi-tenant isolation, perfect data boundaries │ └──────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────┐ │ 2. PEOPLE → Who can do what? │ │ Authorization, governance, human intent │ └──────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────┐ │ 3. THINGS → What exists? │ │ Domain entities (66 types) │ └──────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────┐ │ 4. CONNECTIONS → How do things relate? │ │ Relationships (25 types) │ └──────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────┐ │ 5. EVENTS → What happened? │ │ Audit trail, complete history (67 types) │ └──────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────┐ │ 6. KNOWLEDGE → What does it mean? │ │ AI intelligence, embeddings, RAG │ └──────────────────────────────────────────────────────┘ ``` **Key Principle:** Every feature maps to these 6 dimensions. If you can't map it, you're thinking about it wrong. ### Example: User Creates a Course **Groups (Dimension 1):** ```typescript groupId: "fitnesspro_123"; // All operations scoped here ``` **People (Dimension 2):** ```typescript // Separate people table (NOT things!) { _id: Id<'people'>, email: "sarah@fitnesspro.com", role: "org_owner", // Authorization level groupId: "fitnesspro_123" } ``` **Things (Dimension 3):** ```typescript // Course is a thing { _id: Id<'things'>, thingType: "course", name: "Fitness Fundamentals", groupId: "fitnesspro_123", properties: { description: "...", price: 99, duration: "8 weeks" } } ``` **Connections (Dimension 4):** ```typescript // Sarah owns the course { _id: Id<'connections'>, fromPersonId: sarah_person_id, // Person ID (Dimension 2)! toThingId: course_thing_id, // Thing ID (Dimension 3) relationshipType: "owns", groupId: "fitnesspro_123" } ``` **Events (Dimension 5):** ```typescript // Log the creation { _id: Id<'events'>, eventType: "course_created", actorId: sarah_person_id, // Person who did it (REQUIRED) targetId: course_thing_id, // What was created groupId: "fitnesspro_123", timestamp: Date.now() } ``` **Knowledge (Dimension 6):** ```typescript // Embed course content for AI { _id: Id<'knowledge'>, knowledgeType: "document", text: "Fitness Fundamentals...", embedding: [...], // 768-dim vector sourceThingId: course_thing_id, groupId: "fitnesspro_123", labels: ["course", "fitness", "beginner"] } ``` **Result:** One operation touches all 6 dimensions → complete context for AI agents. --- ## Effect.ts + DataProvider Pattern **The separation uses Effect.ts for type-safety and DataProvider for backend-agnosticism.** ### DataProvider Interface (Backend-Agnostic) ```typescript // frontend/src/providers/DataProvider.ts import { Effect, Context } from "effect"; // Universal interface ALL backends implement export interface DataProvider { // Dimension 1: Organizations organizations: { get: (id: string) => Effect.Effect<Organization, OrganizationNotFoundError>; list: (params?: { status?: string; }) => Effect.Effect<Organization[], Error>; }; // Dimension 2: People (separate from things!) people: { get: (id: string) => Effect.Effect<Person, PersonNotFoundError>; list: (params: { organizationId?: string; role?: string; }) => Effect.Effect<Person[], Error>; create: (input: { email: string; displayName: string; role: string; organizationId: string; }) => Effect.Effect<string, Error>; }; // Dimension 3: Things things: { get: (id: string) => Effect.Effect<Thing, ThingNotFoundError>; list: (params: { type: ThingType; organizationId?: string; }) => Effect.Effect<Thing[], Error>; create: (input: { type: ThingType; name: string; organizationId: string; properties: any; }) => Effect.Effect<string, Error>; }; // Dimension 4: Connections connections: { create: (input: { fromPersonId?: string; // Can connect people fromThingId?: string; // OR things toThingId: string; relationshipType: ConnectionType; organizationId: string; metadata?: any; }) => Effect.Effect<string, Error>; }; // Dimension 5: Events events: { log: (event: { type: EventType; actorId: string; // Always a Person ID! targetId?: string; organizationId: string; metadata?: any; }) => Effect.Effect<void, Error>; }; // Dimension 6: Knowledge knowledge: { search: (params: { query: string; organizationId: string; k?: number; }) => Effect.Effect<KnowledgeChunk[], Error>; }; } export const DataProvider = Context.GenericTag<DataProvider>("DataProvider"); ``` ### Effect.ts Service Example ```typescript // backend/services/CourseService.ts import { Effect } from "effect"; import { DataProvider } from "@/providers/DataProvider"; export class CourseService extends Effect.Service<CourseService>()( "CourseService", { effect: Effect.gen(function* () { const provider = yield* DataProvider; return { // Create course → touches all 6 dimensions create: (params: { name: string; creatorId: string; groupId: string; properties: any; }) => Effect.gen(function* () { // 3. Create thing (course) const courseId = yield* provider.things.create({ type: "course", name: params.name, groupId: params.organizationId, properties: params.properties, }); // 4. Create connection (person owns course) yield* provider.connections.create({ fromPersonId: params.creatorId, // Person ID! toThingId: courseId, relationshipType: "owns", groupId: params.organizationId, }); // 5. Log event (course created) yield* provider.events.log({ type: "course_created", actorId: params.creatorId, // Person who did it targetId: courseId, groupId: params.organizationId, metadata: { courseName: params.name, creatorEmail: "sarah@fitnesspro.com", }, }); // 6. Embed for knowledge (AI can find it) // (handled by background job) return courseId; }), }; }), dependencies: [DataProvider], }, ) {} ``` ### Frontend Usage (Backend-Agnostic) ```tsx // frontend/src/components/CreateCourse.tsx import { useEffectRunner } from "@/hooks/useEffectRunner"; import { CourseService } from "@/services/CourseService"; import { Effect } from "effect"; export function CreateCourseForm() { const { run, loading } = useEffectRunner(); const handleSubmit = async (formData: CourseFormData) => { // Define Effect program using CourseService const program = Effect.gen(function* () { const courseService = yield* CourseService; // Create course → touches all 6 dimensions automatically const courseId = yield* courseService.create({ name: formData.name, creatorId: currentUser.id, groupId: currentGroup.id, properties: { description: formData.description, price: formData.price, }, }); return courseId; }); // Run program (frontend uses DataProvider) const courseId = await run(program); // Redirect to course page navigate(`/courses/${courseId}`); }; return <form onSubmit={handleSubmit}>...</form>; } ``` ### Backend Provider Implementations **Convex Provider:** ```typescript // frontend/src/providers/convex/ConvexProvider.ts import { Effect, Layer } from "effect"; import { ConvexHttpClient } from "convex/browser"; import { DataProvider } from "../DataProvider"; export class ConvexProvider implements DataProvider { constructor(private client: ConvexHttpClient) {} things = { create: (input) => Effect.tryPromise({ try: () => this.client.mutation(api.things.create, input), catch: (error) => new Error(String(error)), }), }; people = { create: (input) => Effect.tryPromise({ try: () => this.client.mutation(api.people.create, input), catch: (error) => new Error(String(error)), }), }; // ... other dimensions } export const convexProvider = (config: { url: string }) => Layer.succeed( DataProvider, new ConvexProvider(new ConvexHttpClient(config.url)), ); ``` **Composite Provider (Multi-Backend):** ```typescript // frontend/src/providers/composite/CompositeProvider.ts import { Effect, Layer } from "effect"; import { DataProvider } from "../DataProvider"; export class CompositeProvider implements DataProvider { constructor( private defaultProvider: DataProvider, private routes: Map<ThingType, DataProvider>, ) {} // Route to appropriate provider based on thing type private getProvider(type?: ThingType): DataProvider { if (type && this.routes.has(type)) { return this.routes.get(type)!; } return this.defaultProvider; } things = { get: (id: string) => Effect.gen(function* () { // Fetch thing to determine type const thing = yield* this.defaultProvider.things.get(id); // Route to appropriate provider const provider = this.getProvider(thing.type); if (provider !== this.defaultProvider) { return yield* provider.things.get(id); } return thing; }), list: (params: { type: ThingType; organizationId?: string }) => Effect.gen(function* () { // Route based on thing type const provider = this.getProvider(params.type); return yield* provider.things.list(params); }), create: (input: { type: ThingType; name: string; organizationId: string; properties: any; }) => Effect.gen(function* () { // Route based on thing type const provider = this.getProvider(input.type); return yield* provider.things.create(input); }), }; // Organizations, People, Events, Knowledge → always use default provider organizations = this.defaultProvider.organizations; people = this.defaultProvider.people; connections = this.defaultProvider.connections; events = this.defaultProvider.events; knowledge = this.defaultProvider.knowledge; } export function compositeProvider(config: { default: Layer<DataProvider>; routes: Record<string, Layer<DataProvider> | "default">; }) { return Effect.gen(function* () { // Resolve default provider const defaultProvider = yield* Effect.provide(DataProvider, config.default); // Resolve route providers const routes = new Map<ThingType, DataProvider>(); for (const [type, providerConfig] of Object.entries(config.routes)) { if (providerConfig === "default") { routes.set(type as ThingType, defaultProvider); } else { const provider = yield* Effect.provide(DataProvider, providerConfig); routes.set(type as ThingType, provider); } } return new CompositeProvider(defaultProvider, routes); }).pipe(Effect.map((provider) => Layer.succeed(DataProvider, provider))); } ``` **Real-World Example:** ```typescript // Auth & courses in Convex, blog in WordPress, products in Shopify export default defineConfig({ integrations: [ one({ provider: compositeProvider({ default: convexProvider({ url: env.PUBLIC_CONVEX_URL }), routes: { blog_post: wordpressProvider({ url: "https://blog.yoursite.com", apiKey: env.WP_API_KEY, }), product: shopifyProvider({ store: "yourstore.myshopify.com", accessToken: env.SHOPIFY_ACCESS_TOKEN, }), }, }), }), ], }); ``` **Benefits:** - ✅ **Type-safe**: Compiler enforces all dimensions - ✅ **Composable**: Services build on each other - ✅ **Testable**: Mock layers, not databases - ✅ **Backend-agnostic**: Change provider, not code - ✅ **Multi-backend**: Use best backend for each data type - ✅ **6-dimension aware**: Every operation properly scoped --- ## Backend Options Once your frontend is backend-agnostic (using the DataProvider interface), you have **two paths**: ### Option 1: Self-Hosted Backend **You own and operate the backend.** ``` Your Frontend → DataProvider → Your Backend Choice ├─ Convex (current) ├─ Supabase ├─ WordPress ├─ Notion ├─ Custom API └─ Any backend ``` **When to choose:** - ✅ Full control over data - ✅ Custom backend logic - ✅ Existing infrastructure (WordPress, Supabase, etc.) - ✅ Enterprise security requirements - ✅ No external dependencies **What you maintain:** - Backend infrastructure - Database management - Auth system setup - API deployment - Monitoring & scaling ### Option 2: Use ONE Backend (BaaS) **ONE hosts and operates the backend for you.** ``` Your Frontend → DataProvider → ONE Backend (BaaS) └─ https://api.one.ie ├─ Auth (6 methods) ├─ Database (6 dimensions) ├─ Real-time sync ├─ Multi-tenancy └─ Free tier ``` **When to choose:** - ✅ **Zero backend work** - focus on frontend only - ✅ **Instant auth** - 6 methods (email, Google, GitHub, etc.) - ✅ **Instant database** - 6-dimension ontology ready - ✅ **Free tier** - Start with 10K API calls/month - ✅ **Real-time included** - Convex-powered subscriptions - ✅ **Fast MVP** - Ship in hours, not weeks **Setup (3 steps):** ```bash # 1. Sign up at one.ie → Get API key # 2. Install SDK npm install @oneie/sdk # 3. Configure USE_ONE_BACKEND=true PUBLIC_ONE_API_KEY=ok_live_abc123 PUBLIC_ONE_ORG_ID=org_abc123 ``` **Pricing:** - **Starter:** Free (10K API calls/month, 100 users, 1K things) - **Pro:** $29/month (100K calls, 1K users, 10K things) - **Enterprise:** Custom (unlimited everything) **No lock-in:** Switch between self-hosted and ONE Backend by changing ONE environment variable. ### Comparison | Feature | Self-Hosted | ONE Backend (BaaS) | | ------------------ | -------------- | ----------------------- | | **Setup time** | Days-Weeks | Minutes | | **Auth** | You build | Included (6 methods) | | **Database** | You design | Included (6 dimensions) | | **Real-time** | You configure | Included (Convex) | | **Multi-tenancy** | You implement | Included | | **Cost** | Infrastructure | Free tier / $29/mo | | **Control** | Full | Managed service | | **Data ownership** | You own | You own | | **Migration** | N/A | Export anytime | ### Hybrid Approach **Use both!** Self-host some backends, use ONE Backend for others: ```typescript // astro.config.ts provider: compositeProvider({ default: oneBackendProvider({ apiKey: env.PUBLIC_ONE_API_KEY, // ONE Backend for core data }), routes: { // Blog from your WordPress blog_post: wordpressProvider({ url: env.WP_URL }), // Products from Shopify product: shopifyProvider({ store: env.SHOPIFY_STORE }), }, }); ``` **Best of both worlds:** - ✅ ONE Backend for auth, users, core features (managed) - ✅ Self-hosted backends for specific data sources (control) - ✅ Mix and match based on needs --- ## Benefits of Separation ### 1. Multi-Tenancy Support **Before:** ``` Org A → Frontend A → Convex Deployment A Org B → Frontend B → Convex Deployment B Org C → Frontend C → Convex Deployment C ❌ 3 orgs = 3 Convex deployments (expensive, hard to manage) ``` **After:** ``` Org A → Frontend A ──┐ ├→ Single Backend API → Single Convex Deployment Org B → Frontend B ──┤ (with API key isolation) │ Org C → Frontend C ──┘ ✅ 3 orgs = 1 Convex deployment (cheap, centralized) ``` ### 2. Backend Independence **Before:** - Frontend tightly coupled to Convex - Can't switch to WordPress, Notion, Supabase, etc. - Organizations must use Convex even if they have existing systems - Must learn Convex SDK for every platform ❌ Locked into Convex **After (Backend-Agnostic):** - Frontend uses DataProvider interface - Organizations can use: - **Convex** (real-time, serverless) - **WordPress** (existing CMS) - **Notion** (databases as backend) - **Supabase** (PostgreSQL + real-time) - **Custom backend** (your own API) - Change backend by editing ONE line in `astro.config.ts` ```typescript // Swap backends - frontend code unchanged! provider: convexProvider({ url: "..." }); // OR provider: wordpressProvider({ url: "...", apiKey: "..." }); // OR provider: notionProvider({ apiKey: "...", databaseId: "..." }); ``` ✅ Organizations use their existing infrastructure ### 3. Team Organization **Before:** ``` Full-stack developer needs to know: - Astro + React - Convex-specific hooks and patterns - Convex schema + queries/mutations/actions - Effect.ts - Business logic ``` **After (Backend-Agnostic):** ``` Frontend developer needs to know: - Astro + React - DataProvider interface (universal) - Effect.ts services Backend developer needs to know: - Their chosen backend (WordPress, Convex, Notion, etc.) - How to implement DataProvider for that backend - 6-dimension ontology mapping ``` ✅ Frontend devs don't learn backend-specific details ✅ Organizations can hire developers with their existing tech stack ✅ Clear separation of concerns ### 4. Progressive Migration & Flexibility **Before:** ``` Organization has WordPress site with 10,000 posts Want to use ONE frontend Must migrate ALL data to Convex first ❌ Big-bang migration required ❌ Can't test gradually ❌ Risk losing SEO, existing integrations ``` **After (Backend-Agnostic):** ``` Organization keeps WordPress backend Implements WordPressProvider (maps WP → 6-dimension ontology) Deploys ONE frontend Frontend talks to WordPress via DataProvider ✅ Zero data migration needed ✅ Keep existing WordPress admin, plugins, integrations ✅ Progressive: Start with one page, migrate gradually ✅ Test ONE frontend without touching backend ``` **Real-World Example:** ```typescript // Week 1: Deploy ONE frontend with WordPress backend provider: wordpressProvider({ url: "https://existing-site.com", apiKey: env.WP_API_KEY, }); // Week 50: Migrate to Convex when ready (ONE line change) provider: convexProvider({ url: env.CONVEX_URL, }); // Frontend code? UNCHANGED. No rewrite needed. ``` ✅ Organizations control their migration timeline ✅ No forced backend choice ✅ Test frontend without backend risk --- ## Migration Strategy ### Phase 1: Create DataProvider Interface (Universal API) **Goal:** Define the backend-agnostic interface that ALL providers must implement **Tasks:** 1. Create `/frontend/src/providers/DataProvider.ts` 2. Define interfaces for 6 dimensions: - `organizations: { get, list, update }` - `people: { get, list, create, update, delete }` - `things: { get, list, create, update, delete }` - `connections: { create, getRelated, getCount, delete }` - `events: { log, query }` - `knowledge: { embed, search }` 3. Define error types (ThingNotFoundError, ConnectionCreateError, etc.) 4. Document interface with TypeScript types **Timeline:** 2-3 days **Risk:** None (just interface definition) **Reference:** See `one/knowledge/ontology-frontend.md` for complete DataProvider interface --- ### Phase 2: Implement ConvexProvider (Wrap Existing Working Backend) **Goal:** Wrap existing **working** Convex backend with DataProvider interface (NO functionality changes) **Tasks:** 1. Create `/frontend/src/providers/convex/ConvexProvider.ts` 2. Implement DataProvider interface using Convex client: ```typescript things.get(id) => client.query(api.queries.things.get, { id }) things.create(input) => client.mutation(api.mutations.things.create, input) // etc. for all methods ``` 3. Wrap Convex calls in Effect.ts for error handling 4. Create factory function: `convexProvider(config)` 5. Test provider in isolation 6. **Critical:** Run `frontend/tests/auth/*` - all tests must pass **Timeline:** 3-5 days **Risk:** Very Low (just wraps existing working Convex calls, no logic changes) **Success Criteria:** - ✅ ConvexProvider implements DataProvider interface fully - ✅ All existing auth tests pass - ✅ No functionality changes - just adds abstraction layer --- ### Phase 3: Create Effect.ts Service Layer **Goal:** Build generic services that use DataProvider (backend-agnostic) **Tasks:** 1. Create `/frontend/src/services/ThingService.ts` (generic, handles all 66 types) 2. Create `/frontend/src/services/ConnectionService.ts` 3. Create `/frontend/src/services/ClientLayer.ts` (dependency injection) 4. Services delegate to DataProvider: ```typescript export class ThingService extends Effect.Service<ThingService>()({ effect: Effect.gen(function* () { const provider = yield* DataProvider // Backend-agnostic! return { get: (id: string) => provider.things.get(id), list: (type, orgId) => provider.things.list({ type, organizationId: orgId }) } }) }) ``` 5. Create `useEffectRunner` hook for React integration **Timeline:** 3-5 days **Risk:** Low (services just delegate to provider) --- ### Phase 4: Configure Provider in astro.config.ts **Goal:** Wire up ConvexProvider as the active backend **Tasks:** 1. Update `astro.config.ts`: ```typescript import { convexProvider } from "./src/providers/convex"; export default defineConfig({ integrations: [ react(), one({ provider: convexProvider({ url: import.meta.env.PUBLIC_CONVEX_URL, }), }), ], }); ``` 2. Test that services can access provider via Effect.ts layers **Timeline:** 1 day **Risk:** Low (configuration only) --- ### Phase 5: Migrate Frontend Components Gradually **Goal:** Replace Convex hooks with Effect.ts services (page by page) **Before:** ```tsx import { useQuery } from "convex/react"; import { api } from "@/convex/_generated/api"; const courses = useQuery(api.queries.things.list, { type: "course" }); ``` **After:** ```tsx import { useEffectRunner } from "@/hooks/useEffectRunner"; import { ThingService } from "@/services/ThingService"; const { run, loading } = useEffectRunner(); const [courses, setCourses] = useState([]); useEffect(() => { const program = Effect.gen(function* () { const thingService = yield* ThingService; return yield* thingService.list("course", orgId); }); run(program, { onSuccess: setCourses }); }, []); ``` **Migration Order:** 1. Low-traffic pages first (`/about`, `/blog`) 2. Medium-traffic pages (`/courses`, `/products`) 3. High-traffic pages (`/`, `/dashboard`) 4. Authentication pages last (highest risk) **Critical - Auth Test Validation:** ```bash # After migrating ANY page that touches auth npm test frontend/tests/auth/ # If tests fail: # 1. STOP migration # 2. Debug the issue # 3. Fix before continuing # 4. Re-run tests until all pass ``` **Timeline:** 2-4 weeks (gradual, page by page) **Risk:** Medium (test each page thoroughly before deploying) **Safety Net:** Existing auth tests catch regressions immediately --- ### Phase 6: Remove Convex Dependencies from Frontend **Goal:** Clean up frontend - no more direct Convex imports **Tasks:** 1. Verify all pages use Effect.ts services (no direct Convex hooks) 2. Remove `convex` from `frontend/package.json` 3. Delete `frontend/convex/` directory (no longer needed) 4. Update build process (no Convex codegen) 5. Frontend is now backend-agnostic! **Timeline:** 1-2 days **Risk:** Low (if Phase 5 complete) --- ### Phase 7 (Optional): Add Alternative Backend Providers **Goal:** Demonstrate backend flexibility - add WordPress, Notion, Supabase providers **Example - WordPress Provider:** ```typescript // /frontend/src/providers/wordpress/WordPressProvider.ts export class WordPressProvider implements DataProvider { things = { get: (id) => Effect.gen(function* () { const response = yield* Effect.tryPromise({ try: () => fetch(`${this.baseUrl}/wp-json/wp/v2/posts/${id}`), catch: (error) => new Error(String(error)), }); const post = yield* Effect.tryPromise({ try: () => response.json(), catch: (error) => new Error(String(error)), }); // Transform WordPress post → ONE thing return { _id: post.id.toString(), type: "post", name: post.title.rendered, properties: { content: post.content.rendered, publishedAt: post.date, }, status: post.status, createdAt: new Date(post.date).getTime(), updatedAt: new Date(post.modified).getTime(), }; }), }; // ... implement other methods } ``` **Swap backends in config:** ```typescript // Change from Convex to WordPress - ONE line! export default defineConfig({ integrations: [ one({ // provider: convexProvider({ url: "..." }) // OLD provider: wordpressProvider({ // NEW url: "https://existing-site.com", apiKey: env.WP_API_KEY, }), }), ], }); ``` **Timeline:** 1-2 weeks per provider **Risk:** Low (doesn't affect existing Convex setup) **Value:** Demonstrates TRUE backend-agnosticism --- ## Backend Implementations Once you have the DataProvider interface, implementing it for ANY backend follows similar patterns. ### Implementation Patterns **Pattern 1: REST API** - Fetch-based requests - Transform responses to ONE types - Handle auth headers **Pattern 2: GraphQL** - Query/mutation-based - Parse GraphQL responses - Map to ONE ontology **Pattern 3: Direct Database** - SQL/NoSQL queries - Row/document mapping - Connection pooling **Pattern 4: CMS API** - Platform-specific SDKs - Content type mapping - Read-only or full CRUD ### Supported Backends **Databases:** - ✅ Convex (current - real-time, serverless) - ✅ Supabase (PostgreSQL + real-time) - ✅ Neon (serverless Postgres) - ✅ PlanetScale (MySQL) - ✅ MongoDB - ✅ PostgreSQL - ✅ MySQL **CMS Platforms:** - ✅ WordPress + WooCommerce - ✅ Strapi - ✅ Contentful - ✅ Sanity - ✅ Ghost - ✅ Prismic **SaaS/Headless:** - ✅ Notion (databases as backend) - ✅ Airtable - ✅ Google Sheets - ✅ Salesforce - ✅ HubSpot - ✅ Shopify **Custom:** - ✅ Your own REST/GraphQL API - ✅ Legacy systems - ✅ Enterprise backends ### Quick Example: Supabase Provider ```typescript // frontend/src/providers/supabase/SupabaseProvider.ts import { createClient } from "@supabase/supabase-js"; import { Effect } from "effect"; import { DataProvider } from "../DataProvider"; export class SupabaseProvider implements DataProvider { private supabase; constructor(url: string, anonKey: string) { this.supabase = createClient(url, anonKey); } things = { get: (id: string) => Effect.gen(this, function* () { const { data, error } = yield* Effect.tryPromise({ try: () => this.supabase.from("things").select("*").eq("id", id).single(), catch: (err) => new Error(String(err)), }); if (error) { return yield* Effect.fail(new ThingNotFoundError(id)); } // Transform Supabase row → ONE Thing return { _id: data.id, type: data.type, name: data.name, properties: data.properties, status: data.status, createdAt: new Date(data.created_at).getTime(), updatedAt: new Date(data.updated_at).getTime(), organizationId: data.organization_id, }; }), list: (params) => Effect.gen(this, function* () { let query = this.supabase .from("things") .select("*") .eq("type", params.type); if (params.organizationId) { query = query.eq("organization_id", params.organizationId); } const { data, error } = yield* Effect.tryPromise({ try: () => query.limit(params.limit || 10), catch: (err) => new Error(String(err)), }); if (error) { return yield* Effect.fail(new Error(error.message)); } return data.map((row) => ({ _id: row.id, type: row.type, name: row.name, properties: row.properties, status: row.status, createdAt: new Date(row.created_at).getTime(), updatedAt: new Date(row.updated_at).getTime(), organizationId: row.organization_id, })); }), create: (input) => Effect.gen(this, function* () { const { data, error } = yield* Effect.tryPromise({ try: () => this.supabase .from("things") .insert({ type: input.type, name: input.name, properties: input.properties, organization_id: input.organizationId, status: "active", created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }) .select() .single(), catch: (err) => new Error(String(err)), }); if (error) { return yield* Effect.fail(new Error(error.message)); } return data.id; }), update: (id, updates) => { /* ... */ }, delete: (id) => { /* ... */ }, }; connections = { /* ... */ }; events = { /* ... */ }; knowledge = { /* ... */ }; // ✅ Supabase supports real-time! subscriptions = { watchThing: (id: string) => Effect.gen(this, function* () { const channel = this.supabase.channel(`thing:${id}`).on( "postgres_changes", { event: "*", schema: "public", table: "things", filter: `id=eq.${id}`, }, (payload) => { // Emit updates via Observable }, ); yield* Effect.tryPromise({ try: () => channel.subscribe(), catch: (err) => new Error(String(err)), }); // Return observable }), }; } export function supabaseProvider(config: { url: string; anonKey: string }) { return Layer.succeed( DataProvider, new SupabaseProvider(config.url, config.anonKey), ); } ``` **Key Points:** 1. Implement `DataProvider` interface 2. Transform backend data → ONE types (Thing, Connection, Event, Knowledge) 3. Handle errors with Effect.ts 4. Map backend fields → ONE ontology fields 5. Done! Frontend works unchanged. ### Complete Implementation Guide For detailed implementation examples of all supported backends: - **Databases**: See `one/connections/any-backend.md` (sections: Supabase, Neon, PlanetScale, MongoDB) - **CMS**: See `one/connections/any-backend.md` (sections: WordPress, Strapi, Contentful) - **SaaS**: See `one/connections/any-backend.md` (sections: Notion, Airtable, Shopify) - **Custom**: See `one/connections/any-backend.md` (section: Custom Backends) ### Authentication by Backend **Each backend handles auth differently:** | Backend | Auth Method | | ---------------------- | ---------------------------------------- | | **Convex** | Better Auth sessions + JWT validation | | **Supabase** | Row Level Security (RLS) + Supabase Auth | | **WordPress** | Application Passwords or OAuth | | **Notion** | Integration tokens (server-side only) | | **ONE Backend (BaaS)** | API keys + Better Auth (6 methods) | | **Custom API** | Your auth system | **Frontend:** Uses provider's auth configuration, not backend-specific code. --- ## Testing Strategy ### 0. Preserve Existing Auth Tests (Critical) **REQUIREMENT:** All tests in `frontend/tests/auth/*` must continue to pass throughout migration. **Strategy:** ```typescript // Run existing auth tests after each phase npm test frontend/tests/auth/ // Tests must pass: // ✅ Signup flow // ✅ Signin flow // ✅ Session management // ✅ Password reset // ✅ Email verification // ✅ Role-based authorization ``` **Migration Safety:** 1. **Phase 2 (ConvexProvider):** Run auth tests - should pass unchanged 2. **Phase 3 (Service Layer):** Run auth tests - should pass with new services 3. **Phase 5 (Component Migration):** Run auth tests after each page migration 4. **Phase 6 (Remove Convex):** Run auth tests - final verification **If auth tests fail at any phase:** STOP, investigate, fix before continuing. --- ### Mock Provider for Testing ```typescript // backend/api/__tests__/tokens.test.ts import { describe, it, expect } from "vitest"; import app from "../index"; describe("Tokens API", () => { it("should require API key", async () => { const res = await app.request("/api/v1/tokens/123"); expect(res.status).toBe(401); }); it("should fetch token with valid key", async () => { const res = await app.request("/api/v1/tokens/123", { headers: { Authorization: `Bearer ${process.env.TEST_API_KEY}`, }, }); expect(res.status).toBe(200); }); it("should enforce org isolation", async () => { const res = await app.request("/api/v1/tokens/org-b-token", { headers: { Authorization: `Bearer ${process.env.ORG_A_API_KEY}`, }, }); expect(res.status).toBe(403); }); }); ``` ### 2. Frontend Integration Tests ```typescript // frontend/src/lib/api/__tests__/client.test.ts import { describe, it, expect, vi } from "vitest"; import { ApiClient } from "../client"; describe("ApiClient", () => { it("should add Authorization header", async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ id: "123" }), }); global.fetch = fetchMock; const client = new ApiClient("sk_test_123"); await client.tokens.get("123"); expect(fetchMock).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer sk_test_123", }), }), ); }); it("should throw ApiError on failure", async () => { global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 404, json: async () => ({ error: "Not found" }), }); const client = new ApiClient("sk_test_123"); await expect(client.tokens.get("999")).rejects.toThrow("Not found"); }); }); ``` --- ## Deployment ### Option 1: Self-Hosted Backend **Example: Deploy with Convex Provider** ```bash # 1. Deploy backend (Convex) cd backend convex deploy # Output: https://your-deployment.convex.cloud # 2. Deploy frontend cd frontend # Set environment variables: # PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud npm run build # Deploy to Vercel/Netlify/Cloudflare Pages ``` **Example: Deploy with Supabase Provider** ```bash # 1. Setup Supabase (already deployed) # Get URL and anon key from Supabase dashboard # 2. Deploy frontend cd frontend # Set environment variables: # PUBLIC_SUPABASE_URL=https://your-project.supabase.co # PUBLIC_SUPABASE_ANON_KEY=your-anon-key npm run build # Deploy to Vercel/Netlify/Cloudflare Pages ``` ### Option 2: Use ONE Backend (BaaS) ```bash # 1. Sign up at one.ie → Get API key # 2. Deploy frontend cd frontend # Set environment variables: # USE_ONE_BACKEND=true # PUBLIC_ONE_API_KEY=ok_live_abc123 # PUBLIC_ONE_ORG_ID=org_abc123 npm run build # Deploy to Vercel/Netlify/Cloudflare Pages ``` **Done!** Backend is managed by ONE, you only deploy frontend. ### Environment Variables by Provider **Convex Provider:** ```env PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud CONVEX_DEPLOYMENT=prod:your-deployment ``` **Supabase Provider:** ```env PUBLIC_SUPABASE_URL=https://your-project.supabase.co PUBLIC_SUPABASE_ANON_KEY=your-anon-key ``` **WordPress Provider:** ```env WORDPRESS_URL=https://yoursite.com WORDPRESS_API_KEY=your-app-password ``` **ONE Backend (BaaS):** ```env USE_ONE_BACKEND=true PUBLIC_ONE_API_KEY=ok_live_abc123 PUBLIC_ONE_ORG_ID=org_abc123 ``` --- ## Summary ### What This Migration Achieves **Before:** - ❌ Frontend tightly coupled to Convex - ❌ Can't swap backends without rewriting code - ❌ Organizations must use Convex (no alternatives) - ❌ No multi-backend support **After:** - ✅ Frontend backend-agnostic (uses DataProvider) - ✅ Swap backends by changing ONE line - ✅ Support ANY backend (Convex, WordPress, Supabase, Notion, etc.) - ✅ Use multiple backends simultaneously - ✅ Option to use ONE Backend (BaaS) - zero backend work - ✅ All existing auth tests pass - ✅ Zero downtime migration - ✅ Can rollback at any phase ### Three Paths Forward **Path 1: Keep Current Convex (No Migration)** - Current system works - No changes needed - Wait for business justification **Path 2: Migrate to Backend-Agnostic (Self-Hosted)** - 4-6 weeks migration - Wrap Convex in DataProvider - Enable future backend swaps - Keep full control **Path 3: Use ONE Backend (BaaS)** - 1 hour setup - No backend to maintain - Free tier available - Focus on frontend only ### References **Detailed Implementation Guides:** - **Any Backend**: See `one/connections/any-backend.md` - Databases: Convex, Supabase, Neon, PlanetScale, MongoDB, PostgreSQL - CMS: WordPress, Strapi, Contentful, Sanity, Ghost - SaaS: Notion, Airtable, Shopify, Salesforce - Custom: Your own REST/GraphQL API - **ONE Backend (BaaS)**: See `one/features/use-one-backend.md` - Setup & onboarding - API key management - Pricing tiers - Migration guide - **6-Dimension Ontology**: See `one/knowledge/ontology.md` - Complete data model - All 66 thing types - All 25 connection types - All 67 event types --- ## Rollback Plan **Key Safety:** We're wrapping a working system, so rollback just means removing the wrapper. If migration encounters issues, rollback is straightforward: **Option 1: Gradual Rollback (Keep Both)** ```typescript // Keep both Convex hooks and Effect.ts services temporarily // Migrate page by page, test thoroughly // Old page (still works) const courses = useQuery(api.queries.things.list, { type: "course" }); // New page (backend-agnostic) const program = Effect.gen(function* () { const thingService = yield* ThingService; return yield* thingService.list("course", orgId); }); ``` **Option 2: Quick Rollback (Revert Provider)** ```typescript // Change provider back to direct Convex hooks if needed // In astro.config.ts: provider: convexProvider({ url: env.PUBLIC_CONVEX_URL }); // Keep this working // Frontend components still work with ConvexProvider // Just wraps the same Convex queries/mutations ``` **Key Safety:** - ConvexProvider just