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.
291 lines (225 loc) • 8.56 kB
Markdown
---
title: Decoupling
dimension: things
category: features
tags: ai, architecture, backend, connections, frontend, groups, things
related_dimensions: connections, 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/DECOUPLING.md
Purpose: Documents frontend/backend decoupling pattern
Related dimensions: connections, groups
For AI agents: Read this to understand DECOUPLING.
---
# Frontend/Backend Decoupling Pattern
**Status:** ✅ Fully Implemented
This document explains the proper architecture for a fully decoupled frontend that can connect to ANY backend (not just our Convex deployment).
## Architecture Layers
```
┌──────────────────────────────────────────────────┐
│ Components (React/Astro) │
│ Use domain hooks │
└────────────────┬─────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────┐
│ src/hooks/* (Domain Hooks) │
│ useGroups, useThings, useConnections │
│ TanStack Query + Effect.ts │
└────────────────┬─────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────┐
│ src/providers/DataProvider (Interface) │
│ Backend-agnostic contract │
└────────────────┬─────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────┐
│ src/providers/convex/ConvexProvider (Impl) │
│ Convex-specific implementation │
└────────────────┬─────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────┐
│ ANY Backend via PUBLIC_CONVEX_URL │
│ - Our backend (shocking-falcon-870.convex.cloud)│
│ - api.one.ie (shared platform backend) │
│ - Customer's own Convex deployment │
└──────────────────────────────────────────────────┘
```
## ❌ Don't Do This (Coupled)
```typescript
// ❌ BAD: Direct Convex imports
import { useQuery } from "convex/react";
import { api } from "convex/_generated/api";
function MyComponent() {
const data = useQuery(api.queries.things.list);
// Tightly coupled to Convex
}
```
## ✅ Do This (Decoupled)
```typescript
// ✅ GOOD: Domain hooks
import { useThings } from "@/hooks/useThings";
function MyComponent() {
const { data, loading, error } = useThings({ type: "course" });
// Works with ANY backend
}
```
## Available Domain Hooks
### Core Ontology Hooks
- `useThings(filter?, options?)` - List/query things (entities)
- `useThing(id, options?)` - Get single thing by ID
- `useConnections(filter?, options?)` - List relationships
- `useConnection(id, options?)` - Get single connection
- `useEvents(filter?, options?)` - List events (actions/logs)
- `useKnowledge(filter?, options?)` - RAG/vector search
### Specialized Hooks
- `useGroups(filter?, options?)` - Hierarchical groups
- `useGroup(id, options?)` - Single group
- `useGroupBySlug(slug, options?)` - Group by slug
- `useGroupStats(groupId, options?)` - Group statistics
- `useOrganizations(filter?, options?)` - Multi-tenant orgs
- `usePeople(filter?, options?)` - Users/creators/customers
### Mutations
- `useCreateGroup(options?)` - Create new group
- `useUpdateGroup(groupId, options?)` - Update group
- `useDeleteGroup(groupId, options?)` - Delete group
## Example: GroupCard Component
**Before (Coupled):**
```typescript
import { useQuery } from "convex/react";
import { api } from "convex/_generated/api";
export function GroupCard({ groupId }: { groupId: string }) {
const group = useQuery(api.queries.groups.getById, { groupId });
const stats = useQuery(api.queries.groups.getStats, { groupId });
if (group === undefined) return <Skeleton />;
return <Card>{group.name}</Card>;
}
```
**After (Decoupled):**
```typescript
import { useGroup, useGroupStats } from "@/hooks/useGroups";
export function GroupCard({ groupId }: { groupId: string }) {
const { data: group, loading } = useGroup(groupId);
const { data: stats } = useGroupStats(groupId);
if (loading) return <Skeleton />;
return <Card>{group?.name}</Card>;
}
```
## Hook Usage Patterns
### Query Hook
```typescript
const { data, loading, error, refetch, refetching } = useGroups(
{
type: "community",
status: "active",
visibility: "public",
limit: 50,
},
{
enabled: true, // Conditional fetching
staleTime: 5000, // Cache freshness
refetchInterval: 10000, // Polling
},
);
```
### Mutation Hook
```typescript
const createGroup = useCreateGroup({
onSuccess: () => {
toast.success("Group created!");
},
onError: (error) => {
toast.error(error.message);
},
});
const handleCreate = async () => {
const { data, error } = await createGroup.mutate({
name: "My Group",
slug: "my-group",
type: "community",
settings: {
visibility: "public",
joinPolicy: "open",
},
});
if (error) {
console.error("Failed:", error);
} else {
console.log("Created:", data);
}
};
```
## Benefits of This Architecture
### 1. Backend Flexibility
- Switch from Convex to REST API without changing components
- Connect to different backend deployments via env var
- Support multiple backends simultaneously
### 2. Type Safety
- TypeScript interfaces throughout
- Effect.ts for typed errors
- TanStack Query for predictable state
### 3. Testability
- Mock DataProvider for unit tests
- Test hooks independently
- No Convex-specific test setup needed
### 4. Performance
- TanStack Query caching
- Automatic request deduplication
- Background refetching
### 5. Developer Experience
- Domain-focused APIs (`useGroups` not `useQuery`)
- Consistent error handling
- Loading states built-in
## Environment Configuration
```bash
# .env.local
PUBLIC_CONVEX_URL=https://shocking-falcon-870.convex.cloud
# Or connect to shared platform
PUBLIC_CONVEX_URL=https://api.one.ie
# Or customer's own deployment
PUBLIC_CONVEX_URL=https://customer.convex.cloud
```
## Creating New Hooks
Follow the established pattern in `src/hooks/useGroups.tsx`:
1. Define TypeScript types
2. Create query hooks using `useDataProvider()`
3. Create mutation hooks with cache invalidation
4. Export everything for easy importing
```typescript
// src/hooks/useCourses.tsx
export function useCourses(filter?, options?) {
const provider = useDataProvider();
const queryFn = async () => {
const effect = provider.things.list({
type: "course",
...filter,
});
return await Effect.runPromise(effect);
};
return useQuery({
queryKey: ["courses", filter],
queryFn,
...options,
});
}
```
## Migration Guide
To migrate from Convex hooks to domain hooks:
1. Find: `import { useQuery } from "convex/react"`
2. Replace with appropriate domain hook
3. Update data access patterns (no `undefined` checks needed)
4. Test with different backend URLs
## Summary
**Never import from `convex/react` or `convex/_generated/api` in components.**
Always use:
- `@/hooks/*` for data fetching
- `@/providers/*` for backend abstraction
- `@/types/*` for shared types
This keeps the frontend **100% decoupled** from the backend implementation.