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,745 lines (1,470 loc) • 71.9 kB
Markdown
---
title: Ontology Frontend
dimension: knowledge
category: ontology-frontend.md
tags: 6-dimensions, agent, architecture, backend, frontend, ontology, testing, ui
related_dimensions: connections, events, groups, people, things
scope: global
created: 2025-11-03
updated: 2025-11-03
version: 1.0.0
ai_context: |
This document is part of the knowledge dimension in the ontology-frontend.md category.
Location: one/knowledge/ontology-frontend.md
Purpose: Documents frontend development ontology
Related dimensions: connections, events, groups, people, things
For AI agents: Read this to understand ontology frontend.
---
# Frontend Development Ontology
**Version:** 2.0.0
**Type System:** Formal ontology for backend-agnostic Astro website development
**Paradigm:** Pure declarative type theory + Provider pattern + Context engineering
---
## Table of Contents
1. [Core Axioms](#core-axioms)
2. [Provider Pattern & Context Engineering](#provider-pattern--context-engineering)
3. [Type Hierarchy](#type-hierarchy)
4. [Critical Distinctions](#critical-distinctions)
5. [State Management Hierarchy](#state-management-hierarchy)
6. [DataProvider Interface](#dataprovider-interface-universal-api)
7. [Entity Definitions](#entity-definitions)
8. [Multi-Tenant Architecture](#multi-tenant-architecture)
9. [Caching Ontology](#caching-ontology)
10. [Error Propagation Ontology](#error-propagation-ontology)
11. [Testing Ontology](#testing-ontology)
12. [Pattern Language](#pattern-language)
13. [Type Sync Automation](#type-sync-automation)
14. [Observability Layer](#observability-layer)
15. [Agent Task Mapping](#agent-task-mapping)
16. [Common Mistakes](#common-mistakes)
17. [Summary](#summary)
---
## Core Axioms
### Axiom 1: Everything is a Thing
```
∀x ∈ Frontend → x : Thing
Thing ::= Page | Component | Content | Service | Provider | Configuration
```
### Axiom 2: All Things Have Type
```
type : Thing → TypeID
TypeID ::= String ∈ ontology.things.types
```
### Axiom 3: Things Connect
```
connect : Thing × Thing → Connection
Connection ::= { from: Thing, to: Thing, type: RelationType, metadata: Object }
```
### Axiom 4: Actions Emit Events
```
action : Thing → Event
Event ::= { type: EventType, actor: Thing, target: Thing, timestamp: Time, metadata: Object }
```
### Axiom 5: Patterns Compose
```
compose : Pattern × Pattern → Pattern
Pattern ::= { type: PatternType, inputs: [Thing], outputs: [Thing], transform: Function }
```
### Axiom 6: Backend Agnosticism
```
∀Provider. Provider implements DataProviderInterface → Frontend works with Provider
DataProviderInterface ::= { organizations, people, things, connections, events, knowledge }
```
### Axiom 7: Context Minimalism
```
∀Operation. Load only required types, not implementations
ContextUsed << ContextAvailable
target_reduction: 98%+
```
**Key Principle:** Frontend knows 6-dimension ontology, not backend implementation.
---
## Provider Pattern & Context Engineering
### The Core Insight: Providers ARE Context Loaders
```typescript
{
insight: "Provider pattern = 99.9% context reduction"
// ❌ Traditional: Load entire backend
traditionalApproach: {
load: "Full Convex schema + all implementations + all docs",
tokens: 280_000,
problem: "Frontend context explodes with backend knowledge"
}
// ✅ Provider pattern: Load only interface
providerApproach: {
load: "DataProviderInterface only (contract)",
tokens: 300,
benefit: "Frontend never loads backend implementation"
}
// Backend translation happens at runtime (0 frontend tokens)
execution: {
frontend: "provider.things.get(id)", // Uses interface
provider: "convexClient.query(...)", // Translates to backend
backend: "Database query", // Executes
result: "Frontend context = 300 tokens (not 280k)"
}
result: {
context_reduction: "99.9%",
backend_flexibility: "infinite (swap with config)",
type_safety: "Effect.ts typed errors",
testability: "Mock provider for tests"
}
}
```
### Provider as Just-In-Time Loader
```typescript
// Traditional AI code generation
❌ function generateCourseComponent_Traditional() {
context = {
convexSchema: loadConvexSchema(), // 15,000 tokens
convexMutations: loadMutations(), // 20,000 tokens
convexQueries: loadQueries(), // 18,000 tokens
implementations: loadImplementations() // 50,000 tokens
} // Total: 103,000 tokens
return ai.generate(task, context)
}
// ✅ Provider pattern
✅ function generateCourseComponent_Provider() {
context = {
interface: "DataProviderInterface", // 300 tokens
operations: ["things.get", "things.list"] // Which operations needed
} // Total: 300 tokens
// AI generates code using interface
code = ai.generate(task, context)
// Result: provider.things.get(id) - works with ANY backend
return code
}
// Savings: 103,000 → 300 tokens = 99.7% reduction
```
### Context Engineering Formula
```typescript
{
traditional: {
formula: "ContextSize = Σ(all_files + all_docs + all_examples)",
typical: "50k-300k tokens per request",
problem: "Hits context limits, slow, expensive"
},
provider: {
formula: "ContextSize = interface_definition + operation_signatures",
typical: "300-500 tokens per request",
benefit: "Never hits limits, fast, cheap, infinite backends"
},
improvement: {
context_reduction: "99%+",
cost_reduction: "100x cheaper",
speed_improvement: "10x faster",
backend_flexibility: "∞ backends supported"
}
}
```
---
## Type Hierarchy
### Base Types
```
Thing
├── Artifact // Code artifacts (what agents create)
│ ├── Page
│ │ ├── LandingPage
│ │ ├── BlogIndex
│ │ ├── BlogPost
│ │ ├── AppPage
│ │ ├── AccountPage
│ │ └── APIRoute
│ ├── Component
│ │ ├── UIComponent
│ │ ├── FeatureComponent
│ │ ├── Layout
│ │ └── Island
│ ├── Content
│ │ ├── Collection
│ │ ├── Entry
│ │ └── Schema
│ ├── Service // Effect.ts services (type-safe data operations)
│ │ ├── GenericService // Handles all 66 types (ThingService, ConnectionService)
│ │ └── SpecializedService // Optional convenience (CourseService)
│ ├── Provider // Backend implementations (swap backends)
│ │ ├── ConvexProvider // Convex backend
│ │ ├── WordPressProvider // WordPress REST API
│ │ ├── NotionProvider // Notion databases
│ │ ├── SupabaseProvider // Supabase PostgreSQL
│ │ └── CustomProvider // Any backend
│ └── Configuration // Display & provider config (NOT business logic)
│ ├── DisplayConfig // UI labels, icons, colors
│ └── ProviderConfig // Backend URL, API keys
├── Pattern // Reusable templates
│ ├── PagePattern
│ ├── ComponentPattern
│ ├── DataPattern
│ ├── ServicePattern
│ └── StylePattern
├── Interface // Abstract contracts
│ ├── DataProviderInterface // Universal 6-dimension API
│ └── SubscriptionInterface // Real-time updates (optional)
├── Capability // Agent capabilities
│ ├── Create
│ ├── Read
│ ├── Update
│ └── Delete
└── Context // Execution context
├── TypeContext
├── PatternContext
└── ExampleContext
```
---
## Critical Distinctions
### What Frontend IS
```typescript
type FrontendResponsibility =
| { type: "render"; data: any } // Display UI from data
| { type: "call_provider"; operation: DataOperation } // Call DataProvider (not direct backend)
| { type: "manage_ui_state"; state: UIState } // Form inputs, modals, loading (component-local)
| { type: "route"; path: Path } // Client-side navigation
| { type: "cache_display"; data: CachedData }; // Cache UI data (not business data)
```
### What Frontend IS NOT
```typescript
type FrontendProhibition =
| { type: "database_access"; reason: "Backend-only (provider abstracts)" }
| {
type: "business_logic";
reason: "Backend validates (frontend calls provider)";
}
| { type: "event_logging"; reason: "Backend logs (frontend just triggers)" }
| {
type: "authorization";
reason: "Backend authorizes (frontend just checks state)";
}
| {
type: "data_validation";
reason: "Backend validates (frontend UX hints only)";
}
| {
type: "org_filtering";
reason: "Provider handles (frontend never filters by org)";
}
| {
type: "backend_specific_code";
reason: "Provider abstracts (frontend uses interface)";
};
```
---
## State Management Hierarchy
### Four-Layer State Architecture
```typescript
{
// Level 1: Server State (Provider owns - SOURCE OF TRUTH)
ServerState: {
owner: "Provider (backed by database)",
source: "Backend database",
access: "Query/Mutation via provider.interface only",
examples: [
"course list",
"user profile",
"enrollment count",
"lesson progress"
],
caching: "Provider-level (Cloudflare KV, Edge cache)",
lifetime: "Persistent (until backend changes)",
rule: "NEVER duplicate in frontend state. Query when needed."
},
// Level 2: SSR State (Astro owns - REQUEST SCOPED)
SSRState: {
owner: "Astro page (.astro files)",
source: "Server-side provider fetch",
access: "Props passed to components",
examples: [
"Initial page data (course details)",
"SEO metadata (title, description)",
"Organization context (from subdomain)"
],
lifetime: "One HTTP request (not persisted)",
pattern: `
---
const course = await provider.things.get(id)
---
<Layout title={course.name}>
<CourseDetail course={course} />
</Layout>
`,
rule: "Use for SEO, initial render, server-only data"
},
// Level 3: Island State (React owns - COMPONENT SCOPED)
IslandState: {
owner: "React component (useState/useReducer)",
source: "Component-local logic",
access: "Component-private (not shared)",
examples: [
"Form input values",
"Modal open/closed",
"Dropdown expanded",
"Loading spinner visible"
],
lifetime: "Component mounted (unmount = state destroyed)",
pattern: `
export function CourseForm() {
const [title, setTitle] = useState('')
const [loading, setLoading] = useState(false)
// State dies when component unmounts
}
`,
rule: "UI-only state. Never duplicate server state."
},
// Level 4: Shared Client State (Nanostores owns - SESSION SCOPED)
SharedState: {
owner: "Nanostores (global atoms)",
source: "Cross-component coordination",
access: "Multiple components subscribe",
examples: [
"Sidebar expanded/collapsed",
"Dark/light theme preference",
"User locale (en/es/fr)",
"Toast notifications"
],
lifetime: "Browser session + localStorage",
pattern: `
// stores/layout.ts
export const sidebarExpanded = atom(true)
// Component A
const expanded = useStore(sidebarExpanded)
// Component B (synced with A)
const expanded = useStore(sidebarExpanded)
`,
rule: "UI preferences only. NOT server data."
},
// Anti-Pattern: Duplicating Server State
antiPattern: {
problem: "Storing server data in frontend state",
example: `
❌ const [courses, setCourses] = useState([])
❌ useEffect(() => {
provider.things.list('course').then(setCourses)
}, [])
// Problem: Server state duplicated in frontend
// Problem: Stale data, manual sync, cache invalidation
`,
solution: `
✅ const courses = useQuery(api.courses.list)
// Provider handles caching, revalidation, subscriptions
// Frontend just renders. No state duplication.
`
},
// Golden Rule
rule: "State lives at HIGHEST appropriate level. Don't lift unnecessarily."
// Decision Tree
decisionTree: {
question: "Does this state persist after page reload?",
yes: {
question: "Is this data from backend?",
yes: "ServerState (provider query)",
no: "SharedState (nanostores + localStorage)"
},
no: {
question: "Do multiple components need this state?",
yes: "SharedState (nanostores, session-only)",
no: {
question: "Is this SSR data?",
yes: "SSRState (Astro props)",
no: "IslandState (useState)"
}
}
}
}
```
---
## DataProvider Interface (Universal API)
### Core Contract
```typescript
// ALL backends must implement this interface
interface DataProviderInterface {
// Dimension 1: Organizations
organizations: {
get: (id: string) → Effect<Organization, OrganizationNotFoundError>
list: (params: ListParams) → Effect<Organization[], Error>
update: (id: string, updates: Partial<Organization>) → Effect<void, Error>
}
// Dimension 2: People
people: {
get: (id: string) → Effect<Person, PersonNotFoundError | UnauthorizedError>
list: (params: ListParams) → Effect<Person[], Error>
create: (input: CreatePersonInput) → Effect<string, PersonCreateError>
update: (id: string, updates: Partial<Person>) → Effect<void, Error>
delete: (id: string) → Effect<void, Error>
}
// Dimension 3: Things
things: {
get: (id: string) → Effect<Thing, ThingNotFoundError | UnauthorizedError>
list: (params: { type: ThingType; groupId?: string; filters?: any }) → Effect<Thing[], Error>
create: (input: CreateThingInput) → Effect<string, ThingCreateError>
update: (id: string, updates: Partial<Thing>) → Effect<void, Error>
delete: (id: string) → Effect<void, Error>
}
// Dimension 4: Connections
connections: {
create: (input: CreateConnectionInput) → Effect<string, ConnectionCreateError>
getRelated: (params: { thingId: string; relationshipType: ConnectionType; direction: Direction }) → Effect<Thing[], Error>
getCount: (thingId: string, relationshipType: ConnectionType) → Effect<number, Error>
delete: (id: string) → Effect<void, Error>
}
// Dimension 5: Events
events: {
log: (event: LogEventInput) → Effect<void, Error>
query: (params: EventQueryParams) → Effect<Event[], Error>
}
// Dimension 6: Knowledge
knowledge: {
embed: (params: EmbedParams) → Effect<string, Error>
search: (params: SearchParams) → Effect<KnowledgeMatch[], Error>
}
// Optional: Real-time subscriptions (not all backends support)
subscriptions?: {
watchThing: (id: string) → Effect<Observable<Thing>, Error>
watchList: (type: ThingType, groupId?: string) → Effect<Observable<Thing[]>, Error>
}
}
```
### Provider Algebra
```typescript
// Provider composition
implement : Backend × DataProviderInterface → Provider
// Examples:
ConvexProvider = implement(ConvexBackend, DataProviderInterface)
WordPressProvider = implement(WordPressBackend, DataProviderInterface)
NotionProvider = implement(NotionBackend, DataProviderInterface)
// Swapping backends (change ONE line in config)
config.provider = ConvexProvider({ url: "..." })
// OR
config.provider = WordPressProvider({ url: "...", apiKey: "..." })
// OR
config.provider = NotionProvider({ apiKey: "...", databaseId: "..." })
// Result: Frontend components don't change
```
---
## Entity Definitions
### Services (Effect.ts Layer)
```typescript
// Generic Service (handles ALL 66 thing types)
type GenericService = Service & {
subtype: "generic"
coverage: ThingType[] // All 66 types
operations: CRUD
invariant: {
backend_agnostic: true // Works with ANY provider
type_safe: true // Effect.ts typed errors
composable: true // Combines into workflows
}
}
// Example: ThingService handles courses, lessons, products, EVERYTHING
type ThingService = GenericService & {
name: "ThingService"
operations: {
get: (id: string) → Effect<Thing, ThingNotFoundError>
list: (type: ThingType, orgId?: string) → Effect<Thing[], Error>
create: (input: CreateThingInput) → Effect<string, ThingCreateError>
update: (id: string, updates: any) → Effect<void, Error>
delete: (id: string) → Effect<void, Error>
}
// Delegates to configured provider
implementation: (provider: DataProvider) => ({
get: (id) => provider.things.get(id),
list: (type, groupId) => provider.things.list({ type, groupId }),
// ...
})
}
// Example: ConnectionService handles ALL relationship types
type ConnectionService = GenericService & {
name: "ConnectionService"
operations: {
create: (input: CreateConnectionInput) → Effect<string, ConnectionCreateError>
getRelated: (thingId, type, direction) → Effect<Thing[], Error>
getCount: (thingId, type) → Effect<number, Error>
delete: (id) → Effect<void, Error>
}
implementation: (provider: DataProvider) => ({
create: (input) => provider.connections.create(input),
getRelated: (thingId, type, dir) => provider.connections.getRelated({ thingId, relationshipType: type, direction: dir }),
// ...
})
}
// Specialized Service (OPTIONAL - only add when patterns repeat 3+ times)
type SpecializedService = Service & {
subtype: "specialized"
coverage: ThingType[] // Specific types only
operations: DomainOperations
dependencies: GenericService[] // Composes generic services
invariant: {
adds_convenience: true // Clearer domain vocabulary
reduces_repetition: true // Encapsulates multi-step operations
optional: true // NOT required (can use generic services directly)
}
}
// Example: CourseService (OPTIONAL - only if you repeat these patterns)
type CourseService = SpecializedService & {
name: "CourseService"
coverage: ["course"]
dependencies: [ThingService, ConnectionService]
operations: {
// Convenience: Get course with lessons (vs 2 separate calls)
getCourseWithLessons: (courseId) → Effect<{ course: Thing; lessons: Thing[] }, Error>
// Convenience: Domain vocabulary (vs generic connection.create)
enrollUser: (userId, courseId) → Effect<string, Error>
// Convenience: Specific query (vs generic connection.getCount)
getEnrollmentCount: (courseId) → Effect<number, Error>
}
implementation: (thingService, connectionService) => ({
getCourseWithLessons: (id) => Effect.all([
thingService.get(id),
connectionService.getRelated(id, 'part_of', 'to')
]),
enrollUser: (userId, courseId) => connectionService.create({
fromThingId: userId,
toThingId: courseId,
relationshipType: 'enrolled_in'
}),
getEnrollmentCount: (courseId) => connectionService.getCount(courseId, 'enrolled_in')
})
}
```
### Providers (Backend Implementations)
```typescript
type Provider = {
type: "provider"
backend: Backend // Convex, WordPress, Notion, etc
implements: DataProviderInterface
// Map ontology operations → backend-specific API calls
translation: {
things: {
get: (id) → BackendSpecificCall
list: (params) → BackendSpecificCall
create: (input) → BackendSpecificCall
// ...
}
connections: {
// ...
}
// ... all 6 dimensions
}
invariant: {
implements_interface: true // Must implement DataProviderInterface
handles_6_dimensions: true // Organizations, People, Things, Connections, Events, Knowledge
}
}
// Convex Provider
type ConvexProvider = Provider & {
backend: "convex"
translation: {
things: {
get: (id) => convexClient.query(api.queries.things.get, { id }),
list: (params) => convexClient.query(api.queries.things.list, params),
create: (input) => convexClient.mutation(api.mutations.things.create, input),
// ...
}
// ... map all operations to Convex API
}
}
// WordPress Provider
type WordPressProvider = Provider & {
backend: "wordpress"
translation: {
things: {
get: (id) => fetch(`${url}/wp-json/wp/v2/posts/${id}`)
.then(transformWordPressPost → OneThing),
list: (params) => fetch(`${url}/wp-json/wp/v2/posts?per_page=${params.limit}`)
.then(transformWordPressPosts → OneThings),
create: (input) => fetch(`${url}/wp-json/wp/v2/posts`, {
method: 'POST',
body: transformOneThing → WordPressPost
}),
// ...
}
// ... map all operations to WordPress REST API
}
}
// Notion Provider
type NotionProvider = Provider & {
backend: "notion"
translation: {
things: {
get: (id) => notionClient.pages.retrieve({ page_id: id })
.then(transformNotionPage → OneThing),
list: (params) => notionClient.databases.query({ database_id, page_size: params.limit })
.then(transformNotionPages → OneThings),
create: (input) => notionClient.pages.create({
parent: { database_id },
properties: transformOneThing → NotionProperties
}),
// ...
}
// ... map all operations to Notion API
}
}
```
### Configuration
```typescript
// Display Configuration (UI only, NOT business logic)
type DisplayConfig = Configuration & {
purpose: "ui_presentation";
scope: ThingType[];
properties: {
displayName: string; // "Course" (singular)
displayNamePlural: string; // "Courses" (plural)
icon: LucideIconName; // "BookOpen"
color: TailwindColor; // "green"
primaryField: string; // Which property to show as title
secondaryField: string; // Which property to show as subtitle
imageField: string; // Which property is the image
fields: FieldConfig[]; // Form fields for UI
};
invariant: {
ui_only: true; // NO business logic
backend_agnostic: true; // Works with any provider
display_hints: true; // Labels, placeholders, icons
};
};
// Provider Configuration (backend connection)
type ProviderConfig = Configuration & {
purpose: "backend_connection";
properties: {
provider: ProviderType; // "convex" | "wordpress" | "notion" | etc
url: string; // Backend URL
apiKey?: string; // API key (if needed)
databaseId?: string; // Database ID (for Notion)
// ... other backend-specific config
};
invariant: {
one_line_swap: true; // Change provider in ONE line
environment_vars: true; // Use .env for secrets
};
};
```
---
## Multi-Tenant Architecture
### Subdomain-Based Organization Isolation
```typescript
{
pattern: "subdomain_org_isolation",
problem: "How does provider handle fitnesspro.one.ie vs techcorp.one.ie?",
solution: {
// Middleware extracts orgId from subdomain
middleware: `
// src/middleware.ts
export function onRequest(context, next) {
const subdomain = context.url.hostname.split('.')[0]
const group = await provider.groups.get({ slug: subdomain })
context.locals.groupId = group._id
context.locals.group = group
return next()
}
`,
// Provider auto-injects groupId in ALL queries
provider: `
// Provider automatically scopes ALL queries
class ConvexProvider implements DataProviderInterface {
constructor(private ctx: Context) {}
things = {
list: (params) => {
// Provider ALWAYS adds org filter (frontend never needs to)
return this.client.query(api.things.list, {
...params,
groupId: this.ctx.locals.groupId // Auto-injected
})
},
create: (input) => {
// Provider ALWAYS stamps org on creation
return this.client.mutation(api.things.create, {
...input,
properties: {
...input.properties,
groupId: this.ctx.locals.groupId // Auto-injected
}
})
}
}
}
`,
// Frontend NEVER manually filters by org
frontend: `
// ✅ Frontend code (same for all orgs)
const courses = await provider.things.list({ type: 'course' })
// Provider adds groupId automatically
// ❌ NEVER do this
const courses = await provider.things.list({
type: 'course',
groupId: ctx.locals.groupId // Provider handles this
})
`,
invariants: [
"Provider enforces isolation (not frontend)",
"Frontend never knows about multi-tenancy",
"One frontend codebase for all groups",
"Data isolation enforced at provider level"
]
},
benefits: [
"Frontend code 100% group-agnostic",
"Security enforced by provider (not UI)",
"No group-specific frontend builds",
"Impossible to leak group data (provider enforces)"
]
}
```
---
## Caching Ontology
### Four-Tier Caching Architecture
```typescript
{
// Tier 1: Type Definition Cache (STATIC - INFINITE TTL)
L1_TypeDefinitions: {
what: "Ontology type structures",
ttl: "∞ (types never change at runtime)",
storage: "Memory (startup load)",
size: "200 tokens × 66 types = 13,200 tokens",
hitRate: "99.9%+ (loaded once)",
implementation: `
class TypeDefinitionCache {
private cache = new Map<ThingType, TypeDefinition>()
async load() {
// Load all 66 types at startup (one time)
const types = await import('./ontology/types')
types.forEach(type => this.cache.set(type.name, type))
}
get(type: ThingType): TypeDefinition {
return this.cache.get(type) // Always hits (loaded at startup)
}
}
// Usage: ~13k tokens loaded ONCE, reused for ALL requests
const typeDef = typeCache.get('course') // 200 tokens, instant
`,
benefit: "AI loads 200 tokens (not 15k implementation tokens)"
},
// Tier 2: Provider Interface Cache (STATIC - INFINITE TTL)
L2_ProviderInterface: {
what: "DataProvider method signatures",
ttl: "∞ (interface never changes)",
storage: "Memory (startup load)",
size: "300 tokens (entire interface)",
hitRate: "100% (never changes)",
implementation: `
// Loaded once at startup
const providerInterface = {
things: {
get: "(id: string) → Effect<Thing, Error>",
list: "(params) → Effect<Thing[], Error>",
// ...
}
// ... all 6 dimensions
} // 300 tokens total
// Usage: AI references interface (not backend implementation)
context = { interface: providerInterface } // 300 tokens
`,
benefit: "AI never loads backend implementation (0 tokens)"
},
// Tier 3: SSR Data Cache (DYNAMIC - TTL: 1-60s)
L3_SSRData: {
what: "Server-rendered page data",
ttl: "1-60s (configurable per route)",
storage: "Cloudflare KV / Edge cache",
size: "Variable (full pages or data)",
strategy: "Stale-while-revalidate",
implementation: `
// Astro page with edge caching
export const prerender = false
export async function GET(context) {
const cacheKey = \`course:\${context.params.id}\`
// Check cache
const cached = await context.locals.cache.get(cacheKey)
if (cached && !isStale(cached, 60)) {
return cached
}
// Fetch fresh
const course = await provider.things.get(context.params.id)
// Cache for 60s
await context.locals.cache.set(cacheKey, course, { ttl: 60 })
return course
}
`,
patterns: {
high_traffic_pages: "60s TTL (homepage, popular courses)",
dynamic_pages: "10s TTL (user dashboards)",
real_time_pages: "0s TTL (admin, analytics)"
},
benefit: "Reduces provider calls by 80-95%"
},
// Tier 4: Client Subscriptions (REALTIME - TTL: 0)
L4_RealtimeSubscriptions: {
what: "WebSocket live updates",
ttl: "0 (always fresh)",
storage: "Client memory (ephemeral)",
strategy: "Provider pushes updates",
implementation: `
// React component with real-time subscription
export function EnrollmentCount({ courseId }) {
// Convex subscription (or any provider with subscription support)
const count = useQuery(api.connections.getCount, {
thingId: courseId,
relationshipType: 'enrolled_in'
})
// Updates automatically when backend changes
return <span>{count}</span>
}
`,
when_to_use: [
"Real-time dashboards",
"Live enrollment counts",
"Collaborative editing",
"Chat/notifications"
],
fallback: "If provider doesn't support subscriptions, poll with L3 cache"
},
// Cache Hierarchy Decision Tree
decisionTree: {
question: "Is this ontology type data?",
yes: "L1 (Type Definition Cache) - infinite TTL",
no: {
question: "Is this provider interface?",
yes: "L2 (Provider Interface Cache) - infinite TTL",
no: {
question: "Does data change in real-time?",
yes: "L4 (Real-time Subscriptions) - 0 TTL",
no: {
question: "Is data page-level?",
yes: "L3 (SSR Data Cache) - 1-60s TTL",
no: "No cache (fetch on demand)"
}
}
}
},
// Golden Rule
rule: "Cache closer to definition = less context loaded. Types/interface = cached forever."
}
```
### Cache Invalidation Strategies
```typescript
{
strategies: {
// L1 & L2: Never invalidate (static types/interface)
static: {
invalidation: "None (only invalidate on deployment)",
reason: "Types and interface don't change at runtime"
},
// L3: Time-based + event-based
ssr: {
time_based: "TTL expires → fetch fresh",
event_based: "Backend mutation → invalidate affected pages",
example: `
// When course updated, invalidate course page cache
await provider.things.update(courseId, updates)
await cache.delete(\`course:\${courseId}\`)
`
},
// L4: Automatic (subscription handles)
realtime: {
invalidation: "Provider pushes updates automatically",
reason: "WebSocket connection = always fresh"
}
}
}
```
---
## Error Propagation Ontology
### Full Error Flow: Provider → Service → Component → UI
```typescript
{
errorFlow: "Provider → Service → Component → UI",
// Level 1: Provider Errors (Effect.ts tagged errors)
providerErrors: {
ThingNotFoundError: {
_tag: "ThingNotFoundError",
fields: { thingId: string; type: ThingType },
recovery: "Show 404 page or 'Not Found' component",
httpStatus: 404
},
UnauthorizedError: {
_tag: "UnauthorizedError",
fields: { userId?: string; requiredPermission: string },
recovery: "Redirect to /login with returnUrl",
httpStatus: 401
},
NetworkError: {
_tag: "NetworkError",
fields: { message: string; retryable: boolean },
recovery: "Retry with exponential backoff (if retryable)",
httpStatus: 503
},
RateLimitError: {
_tag: "RateLimitError",
fields: { retryAfter: number },
recovery: "Show 'Try again in X seconds' message",
httpStatus: 429
}
},
// Level 2: Service Errors (Domain-specific, compose provider errors)
serviceErrors: {
pattern: "Transform provider error → domain error",
example: `
// CourseService.ts
getCourseWithLessons(id: string) {
return Effect.gen(function* () {
const course = yield* thingService.get(id)
const lessons = yield* connectionService.getRelated(id, 'part_of', 'to')
return { course, lessons }
}).pipe(
// Transform provider error → domain error
Effect.catchTag('ThingNotFoundError', (e) =>
Effect.fail(new CourseNotFoundError({
courseId: e.thingId,
message: \`Course \${e.thingId} not found\`
}))
)
)
}
`,
benefits: [
"Domain-specific error vocabulary",
"Hides provider implementation details",
"Easier for frontend to handle"
]
},
// Level 3: Component Errors (Catch and render)
componentErrors: {
pattern: "Match error._tag → Render UI component",
example: `
// CourseDetail.tsx
export function CourseDetail({ courseId }: Props) {
const [course, setCourse] = useState<Thing | null>(null)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
Effect.runPromise(
courseService.getCourseWithLessons(courseId)
)
.then(result => setCourse(result.course))
.catch(e => setError(e))
}, [courseId])
// Render based on error type
if (error) {
switch (error._tag) {
case 'CourseNotFoundError':
return <Alert variant="warning">Course not found</Alert>
case 'UnauthorizedError':
return <Alert variant="error">Please log in to view</Alert>
case 'NetworkError':
return <Alert variant="error">Network error. Retrying...</Alert>
default:
return <Alert variant="error">Something went wrong</Alert>
}
}
return <div>{/* Render course */}</div>
}
`
},
// Level 4: UI Errors (User-facing messages)
uiErrors: {
pattern: "Error → User-friendly message + Action",
mapping: {
ThingNotFoundError: {
message: "This {type} doesn't exist",
action: "Go back or search for another",
component: "<Alert404 />"
},
UnauthorizedError: {
message: "You need to log in to see this",
action: "Log in or Sign up",
component: "<LoginPrompt returnUrl={currentPath} />"
},
NetworkError: {
message: "Connection issue",
action: "Retrying automatically...",
component: "<RetryAlert retrying={true} />"
},
RateLimitError: {
message: "Too many requests",
action: "Try again in {retryAfter} seconds",
component: "<RateLimitAlert retryAfter={error.retryAfter} />"
}
}
},
// Error Boundary Pattern
errorBoundary: `
// ErrorBoundary.tsx
export class ErrorBoundary extends React.Component {
state = { error: null }
static getDerivedStateFromError(error) {
return { error }
}
render() {
if (this.state.error) {
return <ErrorFallback error={this.state.error} />
}
return this.props.children
}
}
// Usage: Wrap islands in error boundaries
<ErrorBoundary>
<CourseDetail client:load courseId={id} />
</ErrorBoundary>
`,
// Golden Rule
invariant: "Every error path has explicit UI recovery. No silent failures."
}
```
---
## Testing Ontology
### Three-Layer Testing Strategy
```typescript
{
// Layer 1: Unit Tests (Service logic with mock provider)
unitTests: {
what: "Test service logic in isolation",
mock: "MockProvider (no real backend)",
fast: "Milliseconds per test",
example: `
// tests/unit/services/course.test.ts
import { describe, it, expect } from 'vitest'
import { Effect, Layer } from 'effect'
import { CourseService } from '@/services/CourseService'
import { DataProvider } from '@/providers/DataProvider'
describe('CourseService.getCourseWithLessons', () => {
it('should return course with lessons', async () => {
// Mock provider
const MockProvider = Layer.succeed(DataProvider, {
things: {
get: (id) => Effect.succeed({
_id: id,
type: 'course',
name: 'Test Course'
})
},
connections: {
getRelated: (thingId, type, direction) => Effect.succeed([
{ _id: 'lesson1', type: 'lesson', name: 'Lesson 1' },
{ _id: 'lesson2', type: 'lesson', name: 'Lesson 2' }
])
}
})
// Test service
const result = await Effect.runPromise(
Effect.gen(function* () {
const service = yield* CourseService
return yield* service.getCourseWithLessons('course123')
}).pipe(Effect.provide(MockProvider))
)
expect(result.course.name).toBe('Test Course')
expect(result.lessons).toHaveLength(2)
})
it('should fail with CourseNotFoundError', async () => {
const MockProvider = Layer.succeed(DataProvider, {
things: {
get: (id) => Effect.fail(new ThingNotFoundError({ thingId: id }))
}
})
const result = await Effect.runPromiseExit(
Effect.gen(function* () {
const service = yield* CourseService
return yield* service.getCourseWithLessons('invalid')
}).pipe(Effect.provide(MockProvider))
)
expect(result._tag).toBe('Failure')
expect(result.cause.error._tag).toBe('CourseNotFoundError')
})
})
`,
coverage: [
"Service logic (business rules)",
"Error transformations",
"Effect composition",
"Edge cases"
]
},
// Layer 2: Integration Tests (Service + Real Provider + Test Backend)
integrationTests: {
what: "Test service + provider + real backend",
backend: "Test Convex deployment (isolated database)",
slow: "Seconds per test (real network calls)",
example: `
// tests/integration/course-creation.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { convexProvider } from '@/providers/convex'
import { CourseService } from '@/services/CourseService'
describe('Course Creation Flow', () => {
let testProvider
let testOrgId
beforeEach(async () => {
// Use test Convex deployment
testProvider = convexProvider({
url: process.env.TEST_CONVEX_URL
})
// Create test org
testOrgId = await testProvider.organizations.create({
name: 'Test Org',
slug: 'test-org'
})
})
afterEach(async () => {
// Cleanup test data
await testProvider.organizations.delete(testOrgId)
})
it('should create course with lessons', async () => {
// Create course (real mutation)
const courseId = await testProvider.things.create({
type: 'course',
name: 'Integration Test Course',
properties: { /* thing properties */ }
})
// Create lessons (real mutations)
const lesson1Id = await testProvider.things.create({
type: 'lesson',
name: 'Lesson 1',
properties: { /* thing properties */ }
})
await testProvider.connections.create({
fromThingId: lesson1Id,
toThingId: courseId,
relationshipType: 'part_of'
})
// Verify (real query)
const result = await CourseService.getCourseWithLessons(courseId)
expect(result.course.name).toBe('Integration Test Course')
expect(result.lessons).toHaveLength(1)
expect(result.lessons[0].name).toBe('Lesson 1')
})
})
`,
coverage: [
"Full data flow (service → provider → backend → database)",
"Real mutations and queries",
"Data consistency",
"Backend constraints"
]
},
// Layer 3: E2E Tests (Full UI flow in browser)
e2eTests: {
what: "Test complete user flows in browser",
tool: "Playwright (headless Chrome)",
slow: "10-30s per test (full page loads)",
example: `
// tests/e2e/course-enrollment.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Course Enrollment Flow', () => {
test('user can enroll in course', async ({ page }) => {
// Navigate to course page
await page.goto('/courses/course123')
// Verify course details rendered
await expect(page.locator('h1')).toContainText('Test Course')
// Click enroll button
await page.click('button:has-text("Enroll")')
// Wait for enrollment confirmation
await expect(page.locator('.toast')).toContainText('Enrolled successfully')
// Verify button changed to "Go to Course"
await expect(page.locator('button')).toContainText('Go to Course')
// Navigate to user dashboard
await page.goto('/account')
// Verify course appears in "My Courses"
await expect(page.locator('.my-courses')).toContainText('Test Course')
})
test('unauthorized user prompted to login', async ({ page }) => {
// Navigate as guest
await page.goto('/courses/course123')
// Click enroll
await page.click('button:has-text("Enroll")')
// Should redirect to login
await expect(page).toHaveURL(/\/login/)
// Login form should have returnUrl
await expect(page.locator('form')).toHaveAttribute(
'data-return-url',
'/courses/course123'
)
})
})
`,
coverage: [
"User flows (click, type, navigate)",
"Visual rendering",
"Loading states",
"Error states",
"Authentication flows"
]
},
// Test Pyramid
pyramid: {
unit: "70% (fast, many tests, mock provider)",
integration: "20% (medium, key flows, test backend)",
e2e: "10% (slow, critical paths, real UI)"
},
// CI/CD Strategy
cicd: {
on_pr: "Run unit + integration tests",
on_merge: "Run all tests (including E2E)",
on_deploy: "Run smoke tests (E2E critical paths)"
},
// Golden Rule
invariant: "Mock providers for unit tests. Real providers for integration. Real UI for E2E."
}
```
---
## Pattern Language
### Pattern: Backend-Agnostic Data Access
```typescript
{
id: "backend_agnostic_data_access",
type: DATA_PATTERN,
applicability: (thing) => thing.type === "service" || thing.type === "component",
inputs: [DataProviderInterface],
outputs: [Data],
transform: (operation) => ({
code: `
// Works with ANY backend (Convex, WordPress, Notion, etc.)
const provider = yield* DataProvider
// Call ontology operation (backend translates)
const data = yield* provider.things.get(id)
return data
`
}),
constraints: [
"no_backend_specific_code",
"uses_provider_interface",
"effect_ts_typed"
],
benefits: [
"swap_backends_one_line",
"type_safe_errors",
"testable_via_mock_provider"
]
}
```
### Pattern: Generic vs Specialized Services
```typescript
{
id: "generic_vs_specialized_service_decision",
type: SERVICE_PATTERN,
decision_tree: {
// Start with generic
question: "Do you need to operate on things?",
yes: {
default: "Use ThingService (handles all 66 types)",
code: `
const thingService = yield* ThingService
const course = yield* thingService.get(courseId)
const lesson = yield* thingService.get(lessonId)
`
},
// Add specialized only if needed
question: "Do you repeat the same 2-3 operation sequence 3+ times?",
yes: {
action: "Create SpecializedService",
code: `
// CourseService.ts (OPTIONAL)
export class CourseService {
getCourseWithLessons: (id) => Effect.all([
thingService.get(id),
connectionService.getRelated(id, 'part_of', 'to')
])
}
`
},
no: {
action: "Keep using generic services",
reason: "Don't create specialized services prematurely"
}
},
constraints: [
"always_have_generic_services",
"specialized_services_optional",
"add_specialized_when_patterns_emerge"
]
}
```
### Pattern: Provider Swapping
```typescript
{
id: "provider_swapping",
type: CONFIGURATION_PATTERN,
applicability: (thing) => thing.type === "configuration",
inputs: [ProviderConfig],
outputs: [ConfiguredProvider],
transform: (config) => ({
// astro.config.ts
code: `
import { convexProvider } from './providers/convex'
// import { wordpressProvider } from './providers/wordpress'
// import { notionProvider } from './providers/notion'
export default defineConfig({
integrations: [
one({
// ✅ Change this ONE line to swap backends
provider: convexProvider({
url: import.meta.env.PUBLIC_CONVEX_URL
})
// OR use WordPress:
// provider: wordpressProvider({
// url: 'https://yoursite.com',
// apiKey: import.meta.env.WORDPRESS_API_KEY
// })
})
]
})
`
}),
constraints: [
"one_line_change",
"no_code_changes",
"components_unchanged"
]
}
```
### Pattern: Progressive Enhancement
```typescript
{
id: "progressive_enhancement",
type: UI_PATTERN,
strategy: "Start SSR, enhance with islands, add real-time last",
// Level 1: Base (works without JS)
base: {
layer: "Astro SSR",
code: `
<form action="/api/courses/enroll" method="post">
<input type="hidden" name="courseId" value={courseId} />
<button type="submit">Enroll</button>
</form>
`,
works_without_js: true,
accessibility: "Full keyboard navigation",
seo: "Search engines can see content"
},
// Level 2: Enhanced (adds interactivity with JS)
enhanced: {
layer: "React island",
code: `
<EnrollButton
client:load
courseId={courseId}
initialEnrolled={enrolled}
/>
`,
requires_js: true,
benefit: "Optimistic UI, instant feedback, loading states",
fallback: "Form still works via SSR if JS fails"
},
// Level 3: Real-time (live updates)
realtime: {
layer: "WebSocket subscription",
code: `
export function EnrollmentCount({ courseId }: Props) {
const count = useQuery(api.connections.getCount, {
thingId: courseId,
relationshipType: 'enrolled_in'
})
return <span>{count} enrolled</span>
}
`,
requires_js: true,
requires_subscription: true,
benefit: "Live count updates as users enroll",
fallback: "SSR shows stale count (still functional)"
},
strategy: {
1: "Start with SSR (universal baseline)",
2: "Add islands for interactivity (progressive)",
3: "Add real-time only where needed (rare)",
rule: "Each layer enhances, never replaces previous layer"
}
}
```
---
## Type Sync Automation
### Automatic Frontend/Backend Type Synchronization
```typescript
{
principle: "Backend schema = single source of truth. Frontend derives types.",
pipeline: {
step1: {
where: "Backend",
what: "Define schema (convex/schema.ts)",
code: `
// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server'
import { v } from 'convex/values'
export default defineSchema({
things: defineTable({
type: v.string(),
name: v.string(),
properties: v.any(),
status: v.union(
v.literal('draft'),
v.literal('published'),
v.literal('archived')
),
createdAt: v.number(),
updatedAt: v.number()
})
})
`
},
step2: {
where: "Backend deployment",
what: "Deploy → Auto-generate types",
command: "npx convex deploy",
generates: "convex/_generated/dataModel.d.ts",
code: `
// Auto-generated by Convex
export type Thing = {
_id: Id<'things'>
_creationTime: number
type: string
name: string
properties: any
status: 'draft' | 'published' | 'archived'
createdAt: number
updatedAt: number
}
`
},
step3: {
where: "Frontend",
what: "Import generated types",
code: `
// src/services/ThingService.ts
import type { Id } from '@/convex/_generated/dataModel'
import type { Thing } from '@/convex/_generated/dataModel'
export class ThingService {
get(id: Id<'things'>): Effect<Thing, Error> {
// TypeScript knows exact shape of Thing
}
}
`
},
step4: {
where: "Development",
what: "Hot reload triggers type regeneration",
trigger: "Save convex/schema.ts",
action: "Convex dev server regenerates types",
result: "TypeScript immediately shows errors in frontend"
}
},
validation: {
ci: {
check: "TypeScript compilation",
command: "npx astro check",
fails_if: "Frontend imports types that don't match backend schema",
result: "Can't merge PR if types mismatch"
},
dev: {
watch: "Convex dev server watches schema changes",
regenerates: "Types on every schema save",
editor: "VS Code shows TypeScript errors instantly"
},
deploy: {
blocks: "Can't deploy frontend if types don't match backend",
ensures: "Frontend and backend always in sync"
}
},
benefits: [
"Single source of truth (backend schema)",
"Zero manual type definition duplication",
"Immediate feedback on type mismatches",
"Impossible to deploy mismatched types",
"Refactoring is type-safe (rename propagates)"
],
example_refactor: `
// Backend: Rename field
// convex/schema.ts
things: defineTable({
// name: v.string(), // OLD
title: v.string(), // NEW