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,956 lines (1,651 loc) 57.6 kB
--- title: Api dimension: connections category: api.md tags: auth, backend, frontend related_dimensions: people, things scope: global created: 2025-11-03 updated: 2025-11-03 version: 1.0.0 ai_context: | This document is part of the connections dimension in the api.md category. Location: one/connections/api.md Purpose: Documents one platform - api reference Related dimensions: people, things For AI agents: Read this to understand api. --- # ONE Platform - API Reference **Version:** 1.0.0 **Last Updated:** 2025-01-15 **Purpose:** Complete API reference for Convex backend, service layer, and external integrations --- ## Table of Contents 1. [Convex Backend API](#convex-backend-api) 2. [Effect.ts Service Layer](#effectts-service-layer) 3. [External Service Providers](#external-service-providers) 4. [Frontend Integration](#frontend-integration) 5. [Authentication API](#authentication-api) 6. [Schema Reference](#schema-reference) 7. [Error Handling](#error-handling) --- ## Convex Backend API ### Core Function Types The ONE platform uses Convex for real-time backend operations with three primary function types: #### Queries (Read Operations) **Purpose:** Read data, automatically cached, real-time reactive subscriptions ```typescript // convex/queries/creators.ts import { query } from "./_generated/server"; import { v } from "convex/values"; export const get = query({ args: { id: v.id("entities") }, handler: async (ctx, args) => { const creator = await ctx.db.get(args.id); if (!creator || creator.type !== "creator") { return null; } // Get relationships const contentConnections = await ctx.db .query("connections") .withIndex("from_type", q => q.eq("fromEntityId", args.id) .eq("relationshipType", "authored") ) .collect(); return { ...creator, contentCount: contentConnections.length }; } }); export const list = query({ args: { limit: v.optional(v.number()), niche: v.optional(v.string()) }, handler: async (ctx, args) => { let query = ctx.db .query("entities") .withIndex("by_type", q => q.eq("type", "creator")) .filter(q => q.eq(q.field("status"), "active")); if (args.niche) { query = query.filter(q => q.eq(q.field("properties.niche"), args.niche) ); } return await query.take(args.limit || 20); } }); ``` **Key Features:** - Automatic caching and invalidation - Real-time subscriptions (updates push to clients) - Can run in parallel - Read-only (no database modifications) #### Mutations (Write Operations) **Purpose:** Write/modify data, transactional (all-or-nothing), optimistic UI updates ```typescript // convex/mutations/creators.ts import { mutation } from "./_generated/server"; import { v } from "convex/values"; export const create = mutation({ args: { email: v.string(), name: v.string(), niche: v.array(v.string()) }, handler: async (ctx, args) => { // Check uniqueness const existing = await ctx.db .query("entities") .withIndex("by_type", q => q.eq("type", "creator")) .filter(q => q.eq(q.field("properties.email"), args.email)) .first(); if (existing) { throw new Error(`Creator with email ${args.email} already exists`); } // Create entity const creatorId = await ctx.db.insert("entities", { type: "creator", name: args.name, properties: { email: args.email, username: args.email.split("@")[0], displayName: args.name, niche: args.niche, expertise: [], totalFollowers: 0, totalContent: 0, totalRevenue: 0 }, status: "active", createdAt: Date.now(), updatedAt: Date.now() }); // Log creation event await ctx.db.insert("events", { type: "creator_created", actorId: creatorId, timestamp: Date.now(), metadata: { email: args.email, niche: args.niche } }); return { creatorId }; } }); export const update = mutation({ args: { id: v.id("entities"), bio: v.optional(v.string()), niche: v.optional(v.array(v.string())), avatar: v.optional(v.string()) }, handler: async (ctx, args) => { const { id, ...updates } = args; const current = await ctx.db.get(id); if (!current || current.type !== "creator") { throw new Error("Creator not found"); } // Merge updates const newProperties = { ...current.properties, ...updates }; await ctx.db.patch(id, { properties: newProperties, updatedAt: Date.now() }); // Log update event await ctx.db.insert("events", { type: "creator_updated", actorId: id, timestamp: Date.now(), metadata: { updatedFields: Object.keys(updates) } }); return { success: true }; } }); ``` **Key Features:** - Transactional (all succeed or all fail) - Optimistic UI updates - Validated with Convex validators - Can schedule background actions #### Actions (External API Calls) **Purpose:** Call external services (OpenAI, Stripe, etc.), non-transactional, can be long-running ```typescript // convex/actions/ai/clone-voice.ts import { action } from "../../_generated/server"; import { v } from "convex/values"; import { api } from "../../_generated/api"; export const cloneVoice = action({ args: { creatorId: v.id("entities"), audioSamples: v.array(v.string()) }, handler: async (ctx, args) => { // Call external ElevenLabs API const response = await fetch("https://api.elevenlabs.io/v1/voices/add", { method: "POST", headers: { "xi-api-key": process.env.ELEVENLABS_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ name: `Creator-${args.creatorId}`, files: args.audioSamples, }), }); if (!response.ok) { throw new Error(`ElevenLabs API error: ${response.statusText}`); } const data = await response.json(); const voiceId = data.voice_id; // Update creator entity with voice ID await ctx.runMutation(api.creators.update, { id: args.creatorId, voiceId }); return { voiceId }; } }); ``` **Key Features:** - Can call external services - Can call mutations/queries via `ctx.runMutation()` and `ctx.runQuery()` - Not transactional (no automatic rollback) - Access to `process.env` for secrets #### Internal Functions **Purpose:** Functions only callable from other Convex functions (not from clients) ```typescript // convex/auth.ts (internal functions) import { internalMutation, internalAction } from "./_generated/server"; import { v } from "convex/values"; import { Resend } from "@convex-dev/resend"; import { components } from "./_generated/api"; export const createPasswordResetToken = internalMutation({ args: { email: v.string() }, handler: async (ctx, args) => { const user = await ctx.db .query("users") .withIndex("by_email", q => q.eq("email", args.email)) .unique(); if (!user) return null; const token = crypto.randomUUID(); const expires = Date.now() + 3600000; // 1 hour await ctx.db.insert("verificationTokens", { identifier: args.email, token, expires, type: "password_reset" }); return { token, email: args.email }; } }); export const sendPasswordResetEmailAction = internalAction({ args: { email: v.string(), resetLink: v.string() }, handler: async (ctx, args) => { const resend = new Resend(components.resend, { testMode: false }); await resend.sendEmail(ctx, { from: process.env.RESEND_FROM_EMAIL || "onboarding@resend.dev", to: args.email, subject: "Reset your password", html: ` <h1>Reset your password</h1> <p>Click the link below to reset your password:</p> <a href="${args.resetLink}">Reset Password</a> <p>This link expires in 1 hour.</p> `, }); } }); ``` ### Database Operations #### Basic CRUD ```typescript // Insert const id = await ctx.db.insert("entities", { type: "creator", name: "John Doe", properties: {}, status: "active", createdAt: Date.now(), updatedAt: Date.now() }); // Get by ID const entity = await ctx.db.get(id); // Update (partial) await ctx.db.patch(id, { properties: { bio: "Updated bio" }, updatedAt: Date.now() }); // Replace (full) await ctx.db.replace(id, { type: "creator", name: "John Doe", properties: { bio: "New bio" }, status: "active", createdAt: entity.createdAt, updatedAt: Date.now() }); // Delete await ctx.db.delete(id); ``` #### Queries with Indexes ```typescript // Query by index const creators = await ctx.db .query("entities") .withIndex("by_type", q => q.eq("type", "creator")) .collect(); // Compound index query const activeCreators = await ctx.db .query("entities") .withIndex("by_type_status", q => q.eq("type", "creator").eq("status", "active") ) .collect(); // Filter after index const recentCreators = await ctx.db .query("entities") .withIndex("by_type", q => q.eq("type", "creator")) .filter(q => q.gt(q.field("createdAt"), Date.now() - 86400000)) .order("desc") .take(10); // Unique result const creator = await ctx.db .query("entities") .withIndex("by_type", q => q.eq("type", "creator")) .filter(q => q.eq(q.field("properties.email"), email)) .unique(); // First result (optional) const firstCreator = await ctx.db .query("entities") .withIndex("by_type", q => q.eq("type", "creator")) .first(); ``` #### Pagination ```typescript import { paginationOptsValidator } from "convex/server"; export const listPaginated = query({ args: { paginationOpts: paginationOptsValidator }, handler: async (ctx, args) => { return await ctx.db .query("entities") .withIndex("by_type", q => q.eq("type", "creator")) .order("desc") .paginate(args.paginationOpts); } }); ``` #### Relationship Queries ```typescript // Get all content authored by creator const contentConnections = await ctx.db .query("connections") .withIndex("from_type", q => q.eq("fromEntityId", creatorId) .eq("relationshipType", "authored") ) .collect(); const content = await Promise.all( contentConnections.map(conn => ctx.db.get(conn.toEntityId)) ); // Get all followers of creator const followerConnections = await ctx.db .query("connections") .withIndex("to_type", q => q.eq("toEntityId", creatorId) .eq("relationshipType", "following") ) .collect(); // Bidirectional relationship check const existingConnection = await ctx.db .query("connections") .withIndex("bidirectional", q => q.eq("fromEntityId", userId) .eq("toEntityId", creatorId) .eq("relationshipType", "following") ) .unique(); ``` ### File Storage ```typescript // Generate upload URL (mutation) export const generateUploadUrl = mutation({ args: {}, handler: async (ctx) => { return await ctx.storage.generateUploadUrl(); } }); // Save file reference (mutation) export const saveFile = mutation({ args: { storageId: v.id("_storage"), name: v.string(), type: v.string(), size: v.number() }, handler: async (ctx, args) => { const fileId = await ctx.db.insert("entities", { type: "media_asset", name: args.name, properties: { storageId: args.storageId, fileType: args.type, fileSize: args.size }, status: "active", createdAt: Date.now(), updatedAt: Date.now() }); return { fileId }; } }); // Get file URL (mutation or action) export const getFileUrl = mutation({ args: { storageId: v.id("_storage") }, handler: async (ctx, args) => { return await ctx.storage.getUrl(args.storageId); } }); ``` ### Scheduled Functions (Crons) ```typescript // convex/crons.ts import { cronJobs } from "convex/server"; import { internal } from "./_generated/api"; const crons = cronJobs(); // Every hour crons.interval( "cleanup expired tokens", { minutes: 60 }, internal.auth.cleanupExpiredTokens ); // Every day at midnight UTC crons.daily( "daily content generation", { hourUTC: 0, minuteUTC: 0 }, internal.content.generateDailyContent ); // Every Monday at 9 AM UTC crons.weekly( "weekly analytics report", { hourUTC: 9, minuteUTC: 0, dayOfWeek: "monday" }, internal.analytics.generateWeeklyReport ); // Every month on the 1st at 10 AM UTC crons.monthly( "monthly billing", { hourUTC: 10, minuteUTC: 0, day: 1 }, internal.billing.processMonthlyBilling ); export default crons; ``` --- ## Effect.ts Service Layer ### Service Architecture Effect.ts services provide typed, composable business logic with automatic dependency injection. #### Creating a Service ```typescript // convex/services/tokens/purchase.ts import { Effect } from "effect"; import { ConvexDatabase } from "../core/database"; import { StripeProvider } from "../providers/stripe"; import { BlockchainProvider } from "../providers/blockchain"; export class TokenService extends Effect.Service<TokenService>()( "TokenService", { effect: Effect.gen(function* () { const db = yield* ConvexDatabase; const stripe = yield* StripeProvider; const blockchain = yield* BlockchainProvider; return { purchase: ({ userId, tokenId, amount, usdAmount }: { userId: Id<"entities">; tokenId: Id<"entities">; amount: number; usdAmount: number; }) => Effect.gen(function* () { // All operations must succeed atomically const [payment, tokens] = yield* Effect.all( [ stripe.charge({ amount: usdAmount * 100, currency: "usd", metadata: { userId, tokenId, amount } }), blockchain.mint({ contractAddress: tokenId, toAddress: userId, amount }) ], { concurrency: 2 } ); // Record in database yield* db.insert("events", { type: "tokens_purchased", actorId: userId, targetId: tokenId, timestamp: Date.now(), metadata: { amount, usdAmount, paymentId: payment.id, txHash: tokens.transactionHash } }); // Update balance yield* db.upsert("connections", { fromEntityId: userId, toEntityId: tokenId, relationshipType: "holds_tokens", metadata: { balance: amount } }); return { paymentId: payment.id, txHash: tokens.transactionHash, amount }; }).pipe( // Automatic rollback on error Effect.onError((error) => Effect.all([ stripe.refund(payment.id), blockchain.burn({ contractAddress: tokenId, amount }) ]) ), Effect.retry({ times: 3 }), Effect.timeout("5 minutes"), Effect.withSpan("purchaseTokens", { attributes: { userId, tokenId, amount } }) ) }; }), dependencies: [ ConvexDatabase.Default, StripeProvider.Default, BlockchainProvider.Default ] } ) {} ``` #### Using Services in Convex Functions ```typescript // convex/mutations/tokens.ts import { confect } from "convex-helpers/server/confect"; import { v } from "convex/values"; import { Effect } from "effect"; import { TokenService } from "../services/tokens/purchase"; import { MainLayer } from "../services"; export const purchase = confect.mutation({ args: { tokenId: v.id("entities"), amount: v.number(), usdAmount: v.number() }, handler: (ctx, args) => Effect.gen(function* () { const userId = yield* getUserId(ctx); const tokenService = yield* TokenService; return yield* tokenService.purchase({ userId, tokenId: args.tokenId, amount: args.amount, usdAmount: args.usdAmount }); }).pipe( Effect.provide(MainLayer), Effect.catchTags({ StripeError: (error) => Effect.fail(new ConvexError({ message: `Payment failed: ${error.message}` })), BlockchainError: (error) => Effect.fail(new ConvexError({ message: `Token minting failed: ${error.message}` })) }) ) }); ``` ### Typed Errors ```typescript // convex/services/errors.ts export class NotFoundError { readonly _tag = "NotFoundError"; constructor( readonly message: string, readonly entityId?: string ) {} } export class ValidationError { readonly _tag = "ValidationError"; constructor( readonly field: string, readonly message: string ) {} } export class InsufficientTokensError { readonly _tag = "InsufficientTokensError"; constructor( readonly userId: string, readonly required: number, readonly available: number ) {} } export class PaymentFailedError { readonly _tag = "PaymentFailedError"; constructor( readonly message: string, readonly code?: string ) {} } // Usage with typed error handling Effect.gen(function* () { const result = yield* tokenService.purchase(args); return result; }).pipe( Effect.catchTag("InsufficientTokensError", (error) => Effect.succeed({ error: `You need ${error.required} tokens but only have ${error.available}` }) ), Effect.catchTag("PaymentFailedError", (error) => Effect.succeed({ error: `Payment failed: ${error.message}` }) ) ) ``` --- ## External Service Providers ### OpenAI Provider ```typescript // convex/services/providers/openai.ts import { Effect } from "effect"; export class OpenAIProvider extends Effect.Service<OpenAIProvider>()( "OpenAIProvider", { effect: Effect.gen(function* () { return { chat: ({ systemPrompt, messages, temperature = 0.7 }: { systemPrompt: string; messages: Array<{ role: string; content: string }>; temperature?: number; }) => Effect.tryPromise({ try: async () => { const response = await fetch( "https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "gpt-4-turbo-preview", messages: [ { role: "system", content: systemPrompt }, ...messages ], temperature }), } ); const data = await response.json(); return { content: data.choices[0].message.content, usage: data.usage }; }, catch: (error) => new OpenAIError(String(error)) }), embed: (text: string) => Effect.tryPromise({ try: async () => { const response = await fetch( "https://api.openai.com/v1/embeddings", { method: "POST", headers: { "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "text-embedding-ada-002", input: text }), } ); const data = await response.json(); return data.data[0].embedding; }, catch: (error) => new OpenAIError(String(error)) }) }; }) } ) {} class OpenAIError { readonly _tag = "OpenAIError"; constructor(readonly message: string) {} } ``` ### Stripe Provider ```typescript // convex/services/providers/stripe.ts import { Effect } from "effect"; import Stripe from "stripe"; export class StripeProvider extends Effect.Service<StripeProvider>()( "StripeProvider", { effect: Effect.gen(function* () { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2023-10-16" }); return { charge: ({ amount, currency, metadata }: { amount: number; currency: string; metadata?: Record<string, string>; }) => Effect.tryPromise({ try: async () => { const paymentIntent = await stripe.paymentIntents.create({ amount, currency, metadata }); return paymentIntent; }, catch: (error) => new StripeError(String(error)) }), refund: (paymentIntentId: string) => Effect.tryPromise({ try: async () => { const refund = await stripe.refunds.create({ payment_intent: paymentIntentId }); return refund; }, catch: (error) => new StripeError(String(error)) }) }; }) } ) {} class StripeError { readonly _tag = "StripeError"; constructor(readonly message: string) {} } ``` ### Blockchain Provider ```typescript // convex/services/providers/blockchain.ts import { Effect } from "effect"; import { ethers } from "ethers"; export class BlockchainProvider extends Effect.Service<BlockchainProvider>()( "BlockchainProvider", { effect: Effect.gen(function* () { const provider = new ethers.JsonRpcProvider(process.env.BASE_RPC_URL); const wallet = new ethers.Wallet( process.env.DEPLOYER_PRIVATE_KEY!, provider ); return { mint: ({ contractAddress, toAddress, amount }: { contractAddress: string; toAddress: string; amount: number; }) => Effect.tryPromise({ try: async () => { const contract = new ethers.Contract( contractAddress, ["function mint(address to, uint256 amount)"], wallet ); const tx = await contract.mint(toAddress, amount); const receipt = await tx.wait(); return { transactionHash: receipt.hash, blockNumber: receipt.blockNumber }; }, catch: (error) => new BlockchainError(String(error)) }), burn: ({ contractAddress, amount }: { contractAddress: string; amount: number; }) => Effect.tryPromise({ try: async () => { const contract = new ethers.Contract( contractAddress, ["function burn(uint256 amount)"], wallet ); const tx = await contract.burn(amount); const receipt = await tx.wait(); return { transactionHash: receipt.hash, blockNumber: receipt.blockNumber }; }, catch: (error) => new BlockchainError(String(error)) }) }; }) } ) {} class BlockchainError { readonly _tag = "BlockchainError"; constructor(readonly message: string) {} } ``` --- ## Frontend Integration ### React Components with Convex Hooks ```typescript // src/components/features/tokens/TokenPurchase.tsx import { useQuery, useMutation } from "convex/react"; import { api } from "@/convex/_generated/api"; import { Id } from "@/convex/_generated/dataModel"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { useState } from "react"; interface TokenPurchaseProps { tokenId: Id<"entities">; } export function TokenPurchase({ tokenId }: TokenPurchaseProps) { const token = useQuery(api.tokens.get, { id: tokenId }); const balance = useQuery(api.tokens.getBalance, { tokenId }); const purchase = useMutation(api.tokens.purchase); const [amount, setAmount] = useState(100); const [error, setError] = useState<string | null>(null); const [loading, setLoading] = useState(false); // Loading state if (token === undefined || balance === undefined) { return ( <Card> <CardHeader> <Skeleton className="h-6 w-48" /> </CardHeader> <CardContent> <Skeleton className="h-24 w-full" /> </CardContent> </Card> ); } // Error state if (token === null) { return ( <Card> <CardContent className="py-6"> <Alert variant="destructive"> <AlertDescription>Token not found</AlertDescription> </Alert> </CardContent> </Card> ); } const handlePurchase = async () => { setLoading(true); setError(null); try { await purchase({ tokenId, amount, usdAmount: amount * token.properties.priceUsd }); } catch (err) { setError(err.message); } finally { setLoading(false); } }; return ( <Card> <CardHeader> <CardTitle>Purchase {token.name}</CardTitle> </CardHeader> <CardContent className="space-y-4"> <div> <p className="text-sm text-muted-foreground">Current Balance</p> <p className="text-2xl font-bold">{balance?.amount || 0}</p> </div> <div> <label className="text-sm font-medium">Amount</label> <input type="number" value={amount} onChange={(e) => setAmount(Number(e.target.value))} className="w-full px-3 py-2 border rounded" /> </div> <div> <p className="text-sm text-muted-foreground">Total Cost</p> <p className="text-xl font-bold"> ${(amount * token.properties.priceUsd).toFixed(2)} </p> </div> {error && ( <Alert variant="destructive"> <AlertDescription>{error}</AlertDescription> </Alert> )} <Button onClick={handlePurchase} disabled={loading || amount <= 0} className="w-full" > {loading ? "Processing..." : `Buy ${amount} Tokens`} </Button> </CardContent> </Card> ); } ``` ### Astro Pages with SSR ```astro --- // src/pages/tokens/[id].astro import { ConvexHttpClient } from "convex/browser"; import { api } from "@/convex/_generated/api"; import Layout from "@/layouts/Layout.astro"; import { TokenPurchase } from "@/components/features/tokens/TokenPurchase"; const convex = new ConvexHttpClient(import.meta.env.PUBLIC_CONVEX_URL); const token = await convex.query(api.tokens.get, { id: Astro.params.id as any }); if (!token) { return Astro.redirect("/404"); } --- <Layout title={`${token.name} - Token Details`}> <div class="container mx-auto py-12"> <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> <div> <h1 class="text-4xl font-bold mb-4">{token.name}</h1> <p class="text-lg text-muted-foreground mb-6"> {token.properties.description} </p> <div class="space-y-4"> <div> <p class="text-sm text-muted-foreground">Price</p> <p class="text-2xl font-bold"> ${token.properties.priceUsd.toFixed(2)} </p> </div> <div> <p class="text-sm text-muted-foreground">Total Supply</p> <p class="text-2xl font-bold"> {token.properties.totalSupply.toLocaleString()} </p> </div> </div> </div> <div> <TokenPurchase client:load tokenId={token._id} /> </div> </div> </div> </Layout> ``` --- ## Authentication API ### Better Auth Integration The ONE platform uses Better Auth for authentication, not Convex Auth. #### Auth Configuration ```typescript // convex/auth.config.ts import { betterAuth } from "better-auth"; import { convexAdapter } from "better-auth/adapters/convex"; export const auth = betterAuth({ database: convexAdapter(), emailAndPassword: { enabled: true, requireEmailVerification: true, }, socialProviders: { github: { clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }, google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }, }, }); ``` #### Auth Helpers ```typescript // convex/auth.ts import { query, mutation, internalMutation, internalAction } from "./_generated/server"; import { v } from "convex/values"; import { Resend } from "@convex-dev/resend"; import { components } from "./_generated/api"; import { internal } from "./_generated/api"; export const getUserId = async (ctx: any): Promise<string | null> => { const identity = await ctx.auth.getUserIdentity(); if (!identity) return null; const user = await ctx.db .query("users") .withIndex("by_token", q => q.eq("tokenIdentifier", identity.tokenIdentifier)) .unique(); return user?._id || null; }; export const requestPasswordReset = mutation({ args: { email: v.string(), baseUrl: v.string() }, handler: async (ctx, args) => { const result = await ctx.runMutation(internal.auth.createPasswordResetToken, { email: args.email, }); if (!result) return { success: true }; const resetLink = `${args.baseUrl}/reset-password?token=${result.token}`; await ctx.scheduler.runAfter(0, internal.auth.sendPasswordResetEmailAction, { email: result.email, resetLink, }); return { success: true }; }, }); export const createPasswordResetToken = internalMutation({ args: { email: v.string() }, handler: async (ctx, args) => { const user = await ctx.db .query("users") .withIndex("by_email", q => q.eq("email", args.email)) .unique(); if (!user) return null; const token = crypto.randomUUID(); const expires = Date.now() + 3600000; // 1 hour await ctx.db.insert("verificationTokens", { identifier: args.email, token, expires, type: "password_reset" }); return { token, email: args.email }; }, }); export const sendPasswordResetEmailAction = internalAction({ args: { email: v.string(), resetLink: v.string() }, handler: async (ctx, args) => { const resend = new Resend(components.resend, { testMode: false }); await resend.sendEmail(ctx, { from: process.env.RESEND_FROM_EMAIL || "onboarding@resend.dev", to: args.email, subject: "Reset your password", html: `<a href="${args.resetLink}">Reset Password</a>`, }); }, }); ``` --- ## Schema Reference (6-Dimension Ontology) The ONE platform uses a **6-dimension ontology** where every feature maps to one of these dimensions: ``` ┌──────────────────────────────────────────────────────────────┐ │ ORGANIZATIONS TABLE │ │ Multi-tenant isolation - who owns what at org level │ └──────────────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────────────┐ │ PEOPLE TABLE │ │ Authorization & governance - who can do what │ └──────────────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────────────┐ │ THINGS TABLE │ │ Every "thing" - users, agents, content, tokens, courses │ └──────────────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────────────┐ │ CONNECTIONS TABLE │ │ Every relationship - owns, follows, taught_by, powers │ └──────────────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────────────┐ │ EVENTS TABLE │ │ Every action - purchased, created, viewed, completed │ └──────────────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────────────┐ │ KNOWLEDGE TABLE │ │ Labels + vectors powering RAG & search │ └──────────────────────────────────────────────────────────────┘ ``` **Golden Rule:** If you can't map your feature to these 6 dimensions, you're thinking about it wrong. ### Entities Table **46 entity types** representing all objects in the ONE platform. **What goes in Entities?** - Simple test: If you can point at it and say "this is a ___", it's an entity. - Examples: "This is a creator" ✅ | "This is a relationship" ❌ (that's a connection) ```typescript entities: defineTable({ type: v.union( // Core (3) v.literal("creator"), // Human creator v.literal("ai_clone"), // Digital twin of creator v.literal("audience_member"), // Fan/user // Business Agents (10) v.literal("strategy_agent"), // Vision, planning, OKRs v.literal("research_agent"), // Market, trends, competitors v.literal("marketing_agent"), // Content strategy, SEO v.literal("sales_agent"), // Funnels, conversion v.literal("service_agent"), // Support, onboarding v.literal("design_agent"), // Brand, UI/UX, assets v.literal("engineering_agent"), // Tech, integration v.literal("finance_agent"), // Revenue, costs, forecasting v.literal("legal_agent"), // Compliance, contracts, IP v.literal("intelligence_agent"), // Analytics, insights // Content (7) v.literal("blog_post"), v.literal("video"), v.literal("podcast"), v.literal("social_post"), v.literal("email"), v.literal("course"), v.literal("lesson"), // Products (4) v.literal("digital_product"), v.literal("membership"), v.literal("consultation"), v.literal("nft"), // Community (3) v.literal("community"), v.literal("conversation"), v.literal("message"), // Token (2) v.literal("token"), v.literal("token_contract"), // Knowledge (2) v.literal("knowledge_item"), v.literal("embedding"), // Platform (6) v.literal("website"), v.literal("landing_page"), v.literal("template"), v.literal("livestream"), v.literal("recording"), v.literal("media_asset"), // Business (7) v.literal("payment"), v.literal("subscription"), v.literal("invoice"), v.literal("metric"), v.literal("insight"), v.literal("prediction"), v.literal("report"), // Marketing (5) v.literal("notification"), v.literal("email_campaign"), v.literal("announcement"), v.literal("referral"), v.literal("campaign"), v.literal("lead") ), name: v.string(), // Display name properties: v.any(), // Type-specific data (JSON) status: v.optional(v.union( v.literal("active"), v.literal("inactive"), v.literal("draft"), v.literal("published"), v.literal("archived") )), createdAt: v.number(), updatedAt: v.number(), deletedAt: v.optional(v.number()) }) .index("by_type", ["type"]) .index("by_status", ["status"]) .index("by_type_status", ["type", "status"]) .index("by_created", ["createdAt"]) .index("by_updated", ["updatedAt"]) .searchIndex("search_entities", { searchField: "name", filterFields: ["type", "status"] }) ``` #### Example Entity Properties by Type **Creator:** ```typescript { email: string, username: string, displayName: string, bio?: string, avatar?: string, niche: string[], // ["fitness", "nutrition"] expertise: string[], targetAudience: string, brandColors?: { primary: string, secondary: string, accent: string }, totalFollowers: number, totalContent: number, totalRevenue: number } ``` **AI Clone:** ```typescript { voiceId?: string, voiceProvider?: "elevenlabs" | "azure" | "custom", appearanceId?: string, appearanceProvider?: "d-id" | "heygen" | "custom", systemPrompt: string, temperature: number, knowledgeBaseSize: number, lastTrainingDate: number, totalInteractions: number, satisfactionScore: number } ``` **Token:** ```typescript { contractAddress: string, blockchain: "sui" | "base" | "solana", standard: "ERC20" | "ERC721" | "ERC1155", totalSupply: number, circulatingSupply: number, price: number, marketCap: number, utility: string[], burnRate: number, holders: number, transactions24h: number, volume24h: number } ``` **Course:** ```typescript { title: string, description: string, thumbnail?: string, modules: number, lessons: number, totalDuration: number, price: number, currency: string, tokenPrice?: number, enrollments: number, completions: number, averageRating: number, generatedBy: "ai" | "human" | "hybrid", personalizationLevel: "none" | "basic" | "advanced" } ``` ### Connections Table **24 relationship types** (consolidated from 33). ```typescript connections: defineTable({ fromEntityId: v.id("entities"), toEntityId: v.id("entities"), relationshipType: v.union( // Ownership (2) v.literal("owns"), v.literal("created_by"), // AI (3) v.literal("clone_of"), v.literal("trained_on"), v.literal("powers"), // Content (5) v.literal("authored"), v.literal("generated_by"), v.literal("published_to"), v.literal("part_of"), v.literal("references"), // Community (4) v.literal("member_of"), v.literal("following"), v.literal("moderates"), v.literal("participated_in"), // Business (4) v.literal("manages"), v.literal("reports_to"), v.literal("collaborates_with"), v.literal("assigned_to"), // Token (3) v.literal("holds_tokens"), v.literal("staked_in"), v.literal("earned_from"), // Product (4) v.literal("purchased"), v.literal("enrolled_in"), v.literal("completed"), v.literal("teaching"), // Consolidated (3) v.literal("transacted"), // payment, subscription, invoice v.literal("referred"), // direct, conversion, campaign v.literal("notified"), // email, sms, push, in_app // Media (2) v.literal("featured_in"), v.literal("hosted_on"), // Analytics (3) v.literal("analyzed_by"), v.literal("optimized_by"), v.literal("influences") ), metadata: v.optional(v.any()), createdAt: v.number(), deletedAt: v.optional(v.number()) }) .index("from_entity", ["fromEntityId"]) .index("to_entity", ["toEntityId"]) .index("from_type", ["fromEntityId", "relationshipType"]) .index("to_type", ["toEntityId", "relationshipType"]) .index("bidirectional", ["fromEntityId", "toEntityId", "relationshipType"]) .index("by_created", ["createdAt"]) ``` ### Events Table **35 event types** (consolidated from 54). ```typescript events: defineTable({ type: v.union( // Creator (3) v.literal("creator_created"), v.literal("creator_updated"), v.literal("content_uploaded"), // AI Clone (5) v.literal("clone_created"), v.literal("clone_interaction"), v.literal("clone_generated_content"), v.literal("voice_cloned"), v.literal("appearance_cloned"), // Agent (4) v.literal("agent_created"), v.literal("agent_executed"), v.literal("agent_completed"), v.literal("agent_failed"), // Content (2 - consolidated) v.literal("content_changed"), // created, updated, deleted v.literal("content_interacted"), // viewed, shared, liked // Audience (4) v.literal("user_joined"), v.literal("user_engaged"), v.literal("ugc_created"), v.literal("comment_posted"), // Course (5) v.literal("course_created"), v.literal("course_enrolled"), v.literal("lesson_completed"), v.literal("course_completed"), v.literal("certificate_earned"), // Token (7) v.literal("token_deployed"), v.literal("tokens_purchased"), v.literal("tokens_earned"), v.literal("tokens_burned"), v.literal("tokens_staked"), v.literal("tokens_unstaked"), v.literal("governance_vote"), // Business (3) v.literal("revenue_generated"), v.literal("cost_incurred"), v.literal("referral_made"), // Growth (4) v.literal("viral_share"), v.literal("referral_converted"), v.literal("achievement_unlocked"), v.literal("level_up"), // Analytics (5) v.literal("metric_calculated"), v.literal("insight_generated"), v.literal("prediction_made"), v.literal("optimization_applied"), v.literal("report_generated"), // Consolidated events v.literal("payment_processed"), // initiated, completed, failed, refunded v.literal("subscription_updated"), // started, renewed, cancelled v.literal("livestream_status_changed"), // scheduled, started, ended v.literal("livestream_interaction"), // joined, left, message v.literal("notification_delivered"), // email, sms, push, in_app v.literal("referral_activity"), // created, converted v.literal("lead_captured") ), actorId: v.id("entities"), targetId: v.optional(v.id("entities")), timestamp: v.number(), metadata: v.any() // Event-specific data }) .index("by_type", ["type"]) .index("by_actor", ["actorId"]) .index("by_target", ["targetId"]) .index("by_timestamp", ["timestamp"]) .index("by_actor_type", ["actorId", "type"]) .index("by_target_type", ["targetId", "type"]) ``` ### Tags Table Simple key-value tagging system for categorization. ```typescript tags: defineTable({ entityId: v.id("entities"), key: v.string(), value: v.string() }) .index("by_entity", ["entityId"]) .index("by_key", ["key"]) .index("by_key_value", ["key", "value"]) ``` --- ## Error Handling ### Typed Errors in Effect.ts ```typescript // Define error classes export class NotFoundError { readonly _tag = "NotFoundError"; constructor(readonly message: string) {} } export class ValidationError { readonly _tag = "ValidationError"; constructor( readonly field: string, readonly message: string ) {} } // Use in service Effect.gen(function* () { const user = yield* db.get(userId); if (!user) { return yield* Effect.fail(new NotFoundError("User not found")); } return user; }).pipe( Effect.catchTag("NotFoundError", (error) => Effect.succeed({ error: error.message }) ) ) ``` ### Convex Error Handling ```typescript import { ConvexError } from "convex/values"; export const myMutation = mutation({ handler: async (ctx, args) => { const user = await ctx.db.get(args.userId); if (!user) { throw new ConvexError({ message: "User not found", code: "NOT_FOUND" }); } // ... continue } }); ``` --- ## Environment Variables ### Required Variables ```bash # Convex CONVEX_URL=https://your-deployment.convex.cloud CONVEX_DEPLOYMENT=your-deployment-name # Better Auth BETTER_AUTH_SECRET=your-secret-key BETTER_AUTH_URL=https://your-domain.com # OAuth Providers GITHUB_CLIENT_ID=your-github-client-id GITHUB_CLIENT_SECRET=your-github-client-secret GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secret # Email (Resend) RESEND_API_KEY=re_... RESEND_FROM_EMAIL=noreply@yourdomain.com # Payments (Stripe) STRIPE_SECRET_KEY=sk_... STRIPE_PUBLISHABLE_KEY=pk_... # Blockchain (Base L2) BASE_RPC_URL=https://mainnet.base.org DEPLOYER_PRIVATE_KEY=0x... # AI Services OPENAI_API_KEY=sk-... ELEVENLABS_API_KEY=... ``` --- ## Quick Reference ### Common Validators ```typescript import { v } from "convex/values"; // Primitives v.string() v.number() v.boolean() v.null() v.any() // Complex types v.id("tableName") v.array(v.string()) v.object({ name: v.string(), age: v.number() }) v.union(v.literal("a"), v.literal("b")) v.optional(v.string()) // Pagination paginationOptsValidator ``` ### Best Practices 1. **Always use indexes** for queries on non-_id fields 2. **Email sending** must be in internal actions, scheduled from mutations 3. **Use Effect.ts** for all business logic with multiple steps or external calls 4. **Type all errors** with `_tag` for exhaustive error handling 5. **Validate args** with Convex validators 6. **Log events** for all significant actions 7. **Use .unique()** when expecting exactly one result, `.first()` for optional 8. **Schedule background work** with `ctx.scheduler.runAfter()` 9. **Keep mutations fast** - use actions for long-running operations 10. **Always provide MainLayer** when using Effect.ts services in Convex functions --- ## How Features Map to Ontology Understanding how to map features to the 6-dimension ontology is critical for implementing new functionality. ### Example: AI Clone Creation **Feature Goal:** Allow creators to create AI clones of themselves. **Ontology Mapping:** **Entities Created:** 1. `ai_clone` entity (the digital twin) 2. Multiple `knowledge_item` entities (training data from creator's content) **Connections Created:** 1. creator → ai_clone (`owns`) 2. ai_clone → knowledge_items (`trained_on`) 3. agent → ai_clone (`powers`) - which agent runs the clone **Events Logged:** 1. `clone_created` - when clone entity created 2. `voice_cloned` - when voice cloning completes 3. `appearance_cloned` - when appearance cloning completes **Tags Added:** - Clone inherits creator's `industry` and `skill` tags - Additional tags: `status:training`, `status:active` ### Example: Token Purchase **Feature Goal:** Allow users to purchase creator tokens. **Ontology Mapping:** **Entities Involved:** 1. `token` entity (the token being purchased) 2. `audience_member` entity (the buyer) **Connections Created/Updated:** 1. buyer → token (`holds_tokens`, metadata: { balance: 100, blockchain: "sui" }) **Events Logged:** 1. `tokens_purchased` (amount, blockchain, payment details) 2. `revenue_generated` (for creator) 3. `payment_processed` (fiat) OR blockchain transaction event (crypto) **Tags Added:** - None (tokens already have tags) ### Example: Course Generation **Feature Goal:** AI generates a personalized course for a creator. **Ontology Mapping:** **Entities Created:** 1. `course` entity 2. Multiple `lesson` entities (one per lesson) 3. `content` entities for each lesson (video, text, quiz) **Connections Created:** 1. creator → course (`owns`) 2. ai_agent → course (`generated_by`) 3. ai_clone → course (`teaching`) 4. course → lessons (`part_of`) 5. lessons → content (`part_of`) **Events Logged:** 1. `course_created` 2. `clone_generated_content` (for each lesson) **Tags Added:** - `skill` tags - what the course teaches - `industry` tags - course category - `audience` tags - target skill level - `format` tag - video, text, interactive ### Example: Livestream Session **Feature Goal:** Creator hosts a livestream with AI clone co-host. **Ontology Mapping:** **Entities Created:** 1. `livestream` entity 2. `recording` entity (after stream ends) 3. Multiple `message` entities (chat messages) **Connections Created:** 1. creator → livestream (`owns`) 2. ai_clone → livestream (`participated_in`) 3. livestream → platform (`hosted_on`) 4. viewers → livestream (`participated_in`) **Events Logged:** 1. `livestream_status_changed` (scheduled → started → ended) 2. `livestream_interaction` (viewers joining, leaving, chatting) 3. `content_changed` (recording created after stream) **Tags Added:** - `topic` tags for stream content - `format:live` tag --- ## Query Patterns for Common Operations ### Get Creator with All Their Content ```typescript export const getCreatorWithContent = query({ args: { creatorId: v.id("entities") }, handler: async (ctx, args) => { // Get creator const creator = await ctx.db.get(args.creatorId); if (!creator || creator.type !== "creator") { return null; } // Get all authored connections const authoredConnections = await ctx.db .query("connections") .withIndex("from_type", q => q.eq("fromEntityId", args.creatorId) .eq("relationshipType", "authored") ) .collect(); // Get all content entities const content = await Promise.all( authoredConnections.map(conn => ctx.db.get(conn.toEntityId)) ); // Get follower count const followers = await ctx.db .query("connections") .withIndex("to_type", q => q.eq("toEntityId", args.creatorId) .eq("relationshipType", "following") ) .collect(); return { ...creator, content: content.filter(Boolean), followerCount: followers.length }; } }); ``` ### Get User's Token Holdings ```typescript export const getUserTokenHoldings = query({ args: { userId: v.id("entities") }, handler: async (ctx, args) =>