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,820 lines (1,523 loc) • 61.4 kB
Markdown
---
title: Hono
dimension: knowledge
category: hono.md
tags: architecture, backend
related_dimensions: events, groups, people
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 hono.md category.
Location: one/knowledge/hono.md
Purpose: Documents hono.md - api backend architecture
Related dimensions: events, groups, people
For AI agents: Read this to understand hono.
---
# Hono.md - API Backend Architecture
## Overview
**Hono** is a lightweight, ultrafast web framework that can be used with Convex in two ways:
1. **Convex-Native** (Recommended): Hono runs inside Convex as HTTP actions (`convex/http.ts`)
2. **Separate API**: Hono runs on Cloudflare Workers, calls Convex via `ConvexHttpClient`
This document covers **both approaches** with complete implementation guides.
### Quick Comparison
| Feature | Convex-Native | Separate API |
|---------|---------------|--------------|
| **Deployment** | Single (Convex only) | Two (Workers + Convex) |
| **Access to Convex** | `c.env.runQuery/Mutation` | `ConvexHttpClient` (HTTP) |
| **Infrastructure** | Minimal | More complex |
| **Multi-Tenancy** | Limited | Full support |
| **Team Separation** | Harder | Easier |
| **API Reusability** | Limited | High (web/mobile/desktop) |
| **Official Pattern** | ✅ Yes ([Convex Stack](https://stack.convex.dev/hono-with-convex)) | Custom |
| **Best For** | Single-tenant apps | Multi-tenant platforms |
**Key Principle:** Use **Convex for ALL data storage** (including auth), not external databases.
**Official Documentation:**
- [Hono with Convex (Convex Stack)](https://stack.convex.dev/hono-with-convex) - Official pattern
- [Hono Documentation](https://hono.dev/) - Hono framework docs
- [convex-helpers](https://stack.convex.dev/convex-helpers) - Helper utilities
## Architecture Vision
```
┌─────────────────────────────────────────────────────────┐
│ ASTRO FRONTEND │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Pages, Components, UI (User Customizable) │ │
│ │ - Vibe code with Convex hooks │ │
│ │ - shadcn/ui components │ │
│ │ - Tailwind styling │ │
│ └────────────────┬─────────────────────────────────┘ │
└───────────────────┼─────────────────────────────────────┘
│ HTTP API Calls
▼
┌─────────────────────────────────────────────────────────┐
│ HONO API BACKEND │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Routes (Hono) │ │
│ │ /api/auth/* - Better Auth endpoints │ │
│ │ /api/tokens/* - Token economy endpoints │ │
│ │ /api/agents/* - Agent management │ │
│ │ /api/content/*- Content creation │ │
│ └────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌────────────────┴─────────────────────────────────┐ │
│ │ Business Logic (Effect.ts Services) │ │
│ │ - TokenService, AgentService, etc. │ │
│ │ - Pure functional logic │ │
│ │ - Type-safe error handling │ │
│ └────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌────────────────┴─────────────────────────────────┐ │
│ │ Data Layer (ConvexHttpClient) │ │
│ │ - Queries/mutations via HTTP │ │
│ │ - Better Auth → Convex adapter │ │
│ └────────────────┬─────────────────────────────────┘ │
└───────────────────┼─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ CONVEX BACKEND │
│ - 6-dimension ontology (organizations, people, things, connections, events, knowledge)│
│ - Auth data stored as entities │
│ - Real-time subscriptions │
│ - Typed functions │
└─────────────────────────────────────────────────────────┘
```
## Technology Stack
**Frontend:**
- Astro 5.14+ (SSR pages)
- React 19 (interactive components)
- shadcn/ui (UI components)
- Tailwind CSS v4 (styling)
- Effect.ts (client-side error handling)
**API Backend:**
- Hono (routing on Cloudflare Workers)
- Better Auth (authentication with Convex adapter)
- Effect.ts (100% business logic coverage - see [effect.md](../connections/effect.md))
- ConvexHttpClient (data access via Effect.ts wrapper)
**Data Backend:**
- Convex (real-time database)
- 6-dimension ontology (things, connections, events, knowledge, people, protocols)
**Deployment:**
- Cloudflare Pages (Astro frontend)
- Cloudflare Workers (Hono API)
- Convex Cloud (database)
**Key Principle:** Effect.ts is used throughout the entire pipeline (frontend → API → services → data) for consistent error handling, composability, and testability. See **[effect.md](../connections/effect.md)** for complete Effect.ts coverage patterns.
## Two Deployment Approaches
There are **two ways** to use Hono with Convex, each with different trade-offs:
### Approach 1: Convex-Native (Recommended for Most Apps)
**Pattern:** Hono runs inside Convex as HTTP actions (`convex/http.ts`)
**Pros:**
- ✅ Single deployment (Convex only)
- ✅ Direct access to Convex functions via `c.env.runQuery/runMutation`
- ✅ No separate API infrastructure needed
- ✅ Simpler authentication (uses Convex auth directly)
- ✅ Official Convex pattern (see [stack.convex.dev/hono-with-convex](https://stack.convex.dev/hono-with-convex))
**Cons:**
- ❌ Less flexible for multi-tenancy
- ❌ Harder to separate concerns for large teams
- ❌ Couples API lifecycle to Convex
**Best For:**
- Single-tenant applications
- Simpler architectures
- Teams that want to minimize infrastructure
**Documentation:** See "Convex-Native Implementation" section below.
### Approach 2: Separate API (Cloudflare Workers)
**Pattern:** Hono runs on Cloudflare Workers, calls Convex via `ConvexHttpClient`
**Pros:**
- ✅ Complete separation of frontend and backend
- ✅ Multi-tenancy support (different frontends, shared API)
- ✅ Independent deployment of API and database
- ✅ API can be reused across web, mobile, desktop
- ✅ Better for large teams (frontend/backend specialization)
**Cons:**
- ❌ More infrastructure to manage (Workers + Convex)
- ❌ HTTP overhead calling Convex
- ❌ More complex authentication setup
**Best For:**
- Multi-tenant platforms
- Large teams with frontend/backend separation
- APIs serving multiple client types
**Documentation:** See "Separate API Implementation" section below.
---
## Convex-Native Implementation
This approach runs Hono **inside Convex** as HTTP actions, following the official Convex pattern.
### Setup
**1. Install dependencies:**
```bash
npm install hono convex-helpers @hono/zod-validator zod
```
**2. Create `convex/http.ts`:**
```typescript
import { Hono } from "hono";
import { HonoWithConvex, HttpRouterWithHono } from "convex-helpers/server/hono";
import { ActionCtx } from "./_generated/server";
import { api } from "./_generated/api";
import { cors } from "hono/cors";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
// Hono app with Convex context
const app: HonoWithConvex<ActionCtx> = new Hono();
// CORS middleware
app.use('/api/*', cors({
origin: ['http://localhost:4321', 'https://your-frontend.pages.dev'],
credentials: true,
}));
/**
* GET /api/tokens/:id
* Fetch token via Convex query
*/
app.get("/api/tokens/:id", async (c) => {
const tokenId = c.req.param("id");
// Direct access to Convex via c.env
const token = await c.env.runQuery(api.queries.entities.get, {
id: tokenId
});
if (!token || token.type !== "token") {
return c.json({ error: "Token not found" }, 404);
}
return c.json(token);
});
/**
* POST /api/tokens/purchase
* Purchase tokens with validation
*/
app.post(
"/api/tokens/purchase",
zValidator(
"json",
z.object({
tokenId: z.string(),
amount: z.number().min(1).max(10000),
})
),
async (c) => {
const { tokenId, amount } = c.req.valid("json");
// Get user from auth (example - implement based on your auth strategy)
const userId = c.req.header("X-User-Id");
if (!userId) {
return c.json({ error: "Unauthorized" }, 401);
}
// Call Convex mutation
const result = await c.env.runMutation(api.mutations.tokens.purchase, {
userId,
tokenId,
amount,
});
return c.json(result);
}
);
/**
* GET /api/messages/:userId
* Dynamic route example
*/
app.get("/api/messages/:userId{[0-9]+}", async (c) => {
const userId = c.req.param("userId");
const messages = await c.env.runQuery(api.queries.messages.getByAuthor, {
authorNumber: userId,
});
return c.json(messages);
});
// Export as Convex HTTP router
export default new HttpRouterWithHono(app);
```
**3. Update `convex.json`:**
```json
{
"functions": "convex/",
"node": {
"externalPackages": ["hono"]
}
}
```
### Authentication with Convex Auth
**Using Convex Auth in Hono routes:**
```typescript
import { getAuthUserId } from "@convex-dev/auth/server";
app.post("/api/protected", async (c) => {
// Get authenticated user ID
const userId = await getAuthUserId(c.env);
if (!userId) {
return c.json({ error: "Unauthorized" }, 401);
}
// Use userId in queries/mutations
const data = await c.env.runQuery(api.queries.getUserData, { userId });
return c.json(data);
});
```
### Cookie Handling
```typescript
import { setCookie, getCookie } from 'hono/cookie';
app.post("/api/session", async (c) => {
const { token } = await c.req.json();
// Set cookie
setCookie(c, 'session_token', token, {
httpOnly: true,
secure: true,
maxAge: 86400, // 1 day
sameSite: 'Lax',
});
return c.json({ success: true });
});
app.get("/api/session", async (c) => {
// Get cookie
const token = getCookie(c, 'session_token');
if (!token) {
return c.json({ error: "No session" }, 401);
}
return c.json({ token });
});
```
### Error Handling
```typescript
app.onError((err, c) => {
console.error(`${err}`);
// Handle specific error types
if (err.name === 'ConvexError') {
return c.json({ error: err.message }, 400);
}
return c.json({ error: 'Internal Server Error' }, 500);
});
```
### Benefits of Convex-Native Approach
1. **Direct Function Access**: No HTTP overhead calling Convex
2. **Shared Type Safety**: Same types for routes and Convex functions
3. **Simple Deployment**: Single `npx convex deploy`
4. **Built-in Auth**: Easy integration with Convex Auth
5. **Environment Variables**: Access via `process.env` in actions
### When to Use Convex-Native
✅ **Use this approach when:**
- Building a single-tenant application
- Want simplest possible deployment
- Don't need multi-frontend support
- Team is focused on rapid development
- Frontend and backend tightly coupled
❌ **Don't use when:**
- Need multi-tenancy (different frontends per org)
- Want to reuse API across mobile/web/desktop
- Large team needs frontend/backend separation
- Need API versioning or complex routing
---
## Separate API Implementation (Cloudflare Workers)
This approach runs Hono on **Cloudflare Workers** separately from Convex, providing maximum flexibility.
## Why Hono + Separation?
### Problems Solved
1. **Frontend Lock-In**: Currently Astro is tightly coupled to backend logic
2. **Multi-Tenancy**: Organizations can't easily customize their frontend
3. **Development Speed**: Frontend changes require backend understanding
4. **API Portability**: Backend logic can't be reused across different frontends
### Benefits
1. **Rapid Prototyping**: Users can "vibe code" frontend with Convex hooks without touching backend
2. **Multi-Frontend Support**: Same API serves web, mobile, desktop apps
3. **Clear Contracts**: API endpoints define clear boundaries
4. **Independent Deployment**: Deploy frontend and backend separately
5. **Team Specialization**: Frontend devs work on UI, backend devs work on logic
6. **Single Database**: All data (including auth) in Convex - no separate database needed
## Implementation Pattern
### 1. Hono API Backend Structure
```
api/
├── src/
│ ├── index.ts # Main Hono app
│ ├── auth.ts # Better Auth with Convex adapter
│ ├── routes/
│ │ ├── auth.ts # /api/auth/* routes
│ │ ├── tokens.ts # /api/tokens/* routes
│ │ ├── agents.ts # /api/agents/* routes
│ │ └── content.ts # /api/content/* routes
│ ├── services/
│ │ ├── convex.ts # Convex integration service
│ │ ├── token.ts # Token business logic (Effect.ts)
│ │ ├── agent.ts # Agent business logic (Effect.ts)
│ │ └── content.ts # Content business logic (Effect.ts)
│ └── middleware/
│ ├── auth.ts # Auth middleware
│ └── cors.ts # CORS configuration
├── convex/ # Shared Convex backend
│ ├── schema.ts # 6-dimension ontology schema
│ ├── queries/
│ │ ├── auth.ts # Auth queries (for Better Auth adapter)
│ │ ├── entities.ts
│ │ └── ...
│ ├── mutations/
│ │ ├── auth.ts # Auth mutations (for Better Auth adapter)
│ │ ├── entities.ts
│ │ └── ...
│ └── services/ # Shared Effect.ts services
├── wrangler.toml # Cloudflare Workers config
└── package.json
```
### 2. Installation
```bash
# Create Hono project
npm create hono@latest api
cd api
# Install dependencies
npm install hono
npm install better-auth
npm install convex
npm install effect
npm install @effect/schema
# For development
npm install -D @cloudflare/workers-types
npm install -D wrangler
```
### 3. Better Auth with Convex Adapter
#### `api/src/auth.ts`
```typescript
import { betterAuth } from 'better-auth';
import { ConvexHttpClient } from 'convex/browser';
import { api } from '../../convex/_generated/api';
type CloudflareBindings = {
BETTER_AUTH_URL: string;
BETTER_AUTH_SECRET: string;
CONVEX_URL: string;
GITHUB_CLIENT_ID: string;
GITHUB_CLIENT_SECRET: string;
GOOGLE_CLIENT_ID: string;
GOOGLE_CLIENT_SECRET: string;
};
/**
* Custom Convex adapter for Better Auth
* Stores all auth data in Convex entities table
*/
function createConvexAdapter(convexUrl: string) {
const client = new ConvexHttpClient(convexUrl);
return {
// User operations
async createUser(data: any) {
return client.mutation(api.mutations.auth.createUser, data);
},
async getUser(id: string) {
return client.query(api.queries.auth.getUser, { id });
},
async getUserByEmail(email: string) {
return client.query(api.queries.auth.getUserByEmail, { email });
},
async updateUser(id: string, data: any) {
return client.mutation(api.mutations.auth.updateUser, { id, data });
},
async deleteUser(id: string) {
return client.mutation(api.mutations.auth.deleteUser, { id });
},
// Session operations
async createSession(data: any) {
return client.mutation(api.mutations.auth.createSession, data);
},
async getSession(token: string) {
return client.query(api.queries.auth.getSession, { token });
},
async updateSession(token: string, data: any) {
return client.mutation(api.mutations.auth.updateSession, { token, data });
},
async deleteSession(token: string) {
return client.mutation(api.mutations.auth.deleteSession, { token });
},
// Account operations (OAuth)
async linkAccount(data: any) {
return client.mutation(api.mutations.auth.linkAccount, data);
},
async unlinkAccount(data: any) {
return client.mutation(api.mutations.auth.unlinkAccount, data);
},
// Verification tokens
async createVerificationToken(data: any) {
return client.mutation(api.mutations.auth.createVerificationToken, data);
},
async getVerificationToken(token: string) {
return client.query(api.queries.auth.getVerificationToken, { token });
},
async deleteVerificationToken(token: string) {
return client.mutation(api.mutations.auth.deleteVerificationToken, { token });
},
};
}
export const auth = (env: CloudflareBindings) => {
return betterAuth({
database: createConvexAdapter(env.CONVEX_URL),
baseURL: env.BETTER_AUTH_URL,
secret: env.BETTER_AUTH_SECRET,
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
},
socialProviders: {
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
},
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
},
},
});
};
```
### 4. Convex Auth Mutations/Queries
#### `convex/mutations/auth.ts`
```typescript
import { v } from 'convex/values';
import { mutation } from '../_generated/server';
/**
* Auth mutations for Better Auth Convex adapter
* Stores auth data in entities table following ontology
*/
export const createUser = mutation({
args: {
email: v.string(),
name: v.optional(v.string()),
image: v.optional(v.string()),
emailVerified: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
const userId = await ctx.db.insert('entities', {
type: 'user',
name: args.name || args.email,
properties: {
email: args.email,
image: args.image,
emailVerified: args.emailVerified || false,
role: 'user',
},
status: 'active',
createdAt: Date.now(),
updatedAt: Date.now(),
});
return { id: userId, ...args };
},
});
export const updateUser = mutation({
args: {
id: v.id('entities'),
data: v.object({
email: v.optional(v.string()),
name: v.optional(v.string()),
image: v.optional(v.string()),
emailVerified: v.optional(v.boolean()),
}),
},
handler: async (ctx, args) => {
const user = await ctx.db.get(args.id);
if (!user || user.type !== 'user') {
throw new Error('User not found');
}
await ctx.db.patch(args.id, {
name: args.data.name || user.name,
properties: {
...user.properties,
...args.data,
},
updatedAt: Date.now(),
});
return { id: args.id, ...args.data };
},
});
export const deleteUser = mutation({
args: { id: v.id('entities') },
handler: async (ctx, args) => {
await ctx.db.delete(args.id);
return { success: true };
},
});
export const createSession = mutation({
args: {
userId: v.id('entities'),
token: v.string(),
expiresAt: v.number(),
},
handler: async (ctx, args) => {
const sessionId = await ctx.db.insert('entities', {
type: 'session',
name: `Session for ${args.userId}`,
properties: {
userId: args.userId,
token: args.token,
expiresAt: args.expiresAt,
},
status: 'active',
createdAt: Date.now(),
updatedAt: Date.now(),
});
// Create connection: user → session
await ctx.db.insert('connections', {
fromEntityId: args.userId,
toEntityId: sessionId,
relationshipType: 'has_session',
createdAt: Date.now(),
});
return { id: sessionId, ...args };
},
});
export const updateSession = mutation({
args: {
token: v.string(),
data: v.object({
expiresAt: v.optional(v.number()),
}),
},
handler: async (ctx, args) => {
const session = await ctx.db
.query('entities')
.filter((q) =>
q.and(
q.eq(q.field('type'), 'session'),
q.eq(q.field('properties.token'), args.token)
)
)
.first();
if (!session) {
throw new Error('Session not found');
}
await ctx.db.patch(session._id, {
properties: {
...session.properties,
...args.data,
},
updatedAt: Date.now(),
});
return { token: args.token, ...args.data };
},
});
export const deleteSession = mutation({
args: { token: v.string() },
handler: async (ctx, args) => {
const session = await ctx.db
.query('entities')
.filter((q) =>
q.and(
q.eq(q.field('type'), 'session'),
q.eq(q.field('properties.token'), args.token)
)
)
.first();
if (session) {
await ctx.db.delete(session._id);
}
return { success: true };
},
});
// OAuth account linking
export const linkAccount = mutation({
args: {
userId: v.id('entities'),
provider: v.string(),
providerAccountId: v.string(),
accessToken: v.optional(v.string()),
refreshToken: v.optional(v.string()),
},
handler: async (ctx, args) => {
const accountId = await ctx.db.insert('entities', {
type: 'oauth_account',
name: `${args.provider} account`,
properties: {
provider: args.provider,
providerAccountId: args.providerAccountId,
accessToken: args.accessToken,
refreshToken: args.refreshToken,
},
status: 'active',
createdAt: Date.now(),
updatedAt: Date.now(),
});
// Connection: user → oauth_account
await ctx.db.insert('connections', {
fromEntityId: args.userId,
toEntityId: accountId,
relationshipType: 'linked_account',
createdAt: Date.now(),
});
return { id: accountId, ...args };
},
});
export const unlinkAccount = mutation({
args: {
provider: v.string(),
providerAccountId: v.string(),
},
handler: async (ctx, args) => {
const account = await ctx.db
.query('entities')
.filter((q) =>
q.and(
q.eq(q.field('type'), 'oauth_account'),
q.eq(q.field('properties.provider'), args.provider),
q.eq(q.field('properties.providerAccountId'), args.providerAccountId)
)
)
.first();
if (account) {
await ctx.db.delete(account._id);
}
return { success: true };
},
});
// Verification tokens
export const createVerificationToken = mutation({
args: {
identifier: v.string(), // email
token: v.string(),
expiresAt: v.number(),
},
handler: async (ctx, args) => {
const tokenId = await ctx.db.insert('entities', {
type: 'verification_token',
name: `Verification for ${args.identifier}`,
properties: {
identifier: args.identifier,
token: args.token,
expiresAt: args.expiresAt,
},
status: 'active',
createdAt: Date.now(),
updatedAt: Date.now(),
});
return { id: tokenId, ...args };
},
});
export const deleteVerificationToken = mutation({
args: { token: v.string() },
handler: async (ctx, args) => {
const token = await ctx.db
.query('entities')
.filter((q) =>
q.and(
q.eq(q.field('type'), 'verification_token'),
q.eq(q.field('properties.token'), args.token)
)
)
.first();
if (token) {
await ctx.db.delete(token._id);
}
return { success: true };
},
});
```
#### `convex/queries/auth.ts`
```typescript
import { v } from 'convex/values';
import { query } from '../_generated/server';
/**
* Auth queries for Better Auth Convex adapter
*/
export const getUser = query({
args: { id: v.id('entities') },
handler: async (ctx, args) => {
const user = await ctx.db.get(args.id);
if (!user || user.type !== 'user') return null;
return {
id: user._id,
email: user.properties.email,
name: user.name,
image: user.properties.image,
emailVerified: user.properties.emailVerified,
};
},
});
export const getUserByEmail = query({
args: { email: v.string() },
handler: async (ctx, args) => {
const user = await ctx.db
.query('entities')
.filter((q) =>
q.and(
q.eq(q.field('type'), 'user'),
q.eq(q.field('properties.email'), args.email)
)
)
.first();
if (!user) return null;
return {
id: user._id,
email: user.properties.email,
name: user.name,
image: user.properties.image,
emailVerified: user.properties.emailVerified,
};
},
});
export const getSession = query({
args: { token: v.string() },
handler: async (ctx, args) => {
const session = await ctx.db
.query('entities')
.filter((q) =>
q.and(
q.eq(q.field('type'), 'session'),
q.eq(q.field('properties.token'), args.token)
)
)
.first();
if (!session) return null;
// Check if expired
if (session.properties.expiresAt < Date.now()) {
await ctx.db.delete(session._id);
return null;
}
return {
id: session._id,
userId: session.properties.userId,
token: args.token,
expiresAt: session.properties.expiresAt,
};
},
});
export const getVerificationToken = query({
args: { token: v.string() },
handler: async (ctx, args) => {
const token = await ctx.db
.query('entities')
.filter((q) =>
q.and(
q.eq(q.field('type'), 'verification_token'),
q.eq(q.field('properties.token'), args.token)
)
)
.first();
if (!token) return null;
// Check if expired
if (token.properties.expiresAt < Date.now()) {
await ctx.db.delete(token._id);
return null;
}
return {
identifier: token.properties.identifier,
token: args.token,
expiresAt: token.properties.expiresAt,
};
},
});
```
### 5. Hono Main App
#### `api/src/index.ts`
```typescript
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { auth } from './auth';
import { tokenRoutes } from './routes/tokens';
import { agentRoutes } from './routes/agents';
import { contentRoutes } from './routes/content';
type CloudflareBindings = {
BETTER_AUTH_URL: string;
BETTER_AUTH_SECRET: string;
CONVEX_URL: string;
GITHUB_CLIENT_ID: string;
GITHUB_CLIENT_SECRET: string;
GOOGLE_CLIENT_ID: string;
GOOGLE_CLIENT_SECRET: string;
};
const app = new Hono<{ Bindings: CloudflareBindings }>();
// CORS for Astro frontend
app.use('/*', cors({
origin: ['http://localhost:4321', 'https://your-astro-frontend.pages.dev'],
credentials: true,
}));
// Better Auth endpoints (handles sign-in, sign-up, OAuth, etc.)
app.on(['GET', 'POST'], '/api/auth/*', (c) => {
return auth(c.env).handler(c.req.raw);
});
// Business logic routes
app.route('/api/tokens', tokenRoutes);
app.route('/api/agents', agentRoutes);
app.route('/api/content', contentRoutes);
// Health check
app.get('/health', (c) => c.json({ status: 'ok' }));
export default app;
```
### 6. Convex Integration Service (Effect.ts Wrapper)
#### `api/src/services/convex.ts`
```typescript
import { Effect, Context, Layer } from 'effect';
import { ConvexHttpClient } from 'convex/browser';
import { api } from '../../../convex/_generated/api';
import type { Id } from '../../../convex/_generated/dataModel';
/**
* Convex Database Error
*/
export class ConvexDatabaseError {
readonly _tag = 'ConvexDatabaseError';
constructor(readonly operation: string, readonly cause: unknown) {}
}
/**
* Convex Database Service (Effect.ts wrapper)
* Provides typed access to Convex functions with Effect error handling
*
* See effect.md for complete Effect.ts integration patterns
*/
export class ConvexDatabase extends Context.Tag('ConvexDatabase')<
ConvexDatabase,
{
// Generic operations
query: <T>(name: any, args: any) => Effect.Effect<T, ConvexDatabaseError>;
mutation: <T>(name: any, args: any) => Effect.Effect<T, ConvexDatabaseError>;
action: <T>(name: any, args: any) => Effect.Effect<T, ConvexDatabaseError>;
// Convenience methods for common operations
getEntity: (id: Id<'entities'>) => Effect.Effect<any, ConvexDatabaseError>;
listEntities: (type: string) => Effect.Effect<any[], ConvexDatabaseError>;
}
>() {
/**
* Live implementation (production)
*/
static readonly Live = Layer.effect(
ConvexDatabase,
Effect.gen(function* () {
// Get Convex URL from environment
const convexUrl = yield* Effect.sync(() => process.env.CONVEX_URL);
if (!convexUrl) {
return yield* Effect.fail(
new ConvexDatabaseError('init', 'CONVEX_URL not set')
);
}
const client = new ConvexHttpClient(convexUrl);
return {
// Generic query/mutation/action
query: <T>(name: any, args: any) =>
Effect.tryPromise({
try: () => client.query(name, args) as Promise<T>,
catch: (error) => new ConvexDatabaseError('query', error),
}),
mutation: <T>(name: any, args: any) =>
Effect.tryPromise({
try: () => client.mutation(name, args) as Promise<T>,
catch: (error) => new ConvexDatabaseError('mutation', error),
}),
action: <T>(name: any, args: any) =>
Effect.tryPromise({
try: () => client.action(name, args) as Promise<T>,
catch: (error) => new ConvexDatabaseError('action', error),
}),
// Convenience methods
getEntity: (id: Id<'entities'>) =>
Effect.tryPromise({
try: () => client.query(api.queries.entities.get, { id }),
catch: (error) => new ConvexDatabaseError('getEntity', error),
}),
listEntities: (type: string) =>
Effect.tryPromise({
try: () => client.query(api.queries.entities.list, { type }),
catch: (error) => new ConvexDatabaseError('listEntities', error),
}),
};
})
);
/**
* Create layer from environment
*/
static fromEnv = (convexUrl: string) =>
Layer.succeed(ConvexDatabase, {
query: <T>(name: any, args: any) => {
const client = new ConvexHttpClient(convexUrl);
return Effect.tryPromise({
try: () => client.query(name, args) as Promise<T>,
catch: (error) => new ConvexDatabaseError('query', error),
});
},
mutation: <T>(name: any, args: any) => {
const client = new ConvexHttpClient(convexUrl);
return Effect.tryPromise({
try: () => client.mutation(name, args) as Promise<T>,
catch: (error) => new ConvexDatabaseError('mutation', error),
});
},
action: <T>(name: any, args: any) => {
const client = new ConvexHttpClient(convexUrl);
return Effect.tryPromise({
try: () => client.action(name, args) as Promise<T>,
catch: (error) => new ConvexDatabaseError('action', error),
});
},
getEntity: (id: Id<'entities'>) => {
const client = new ConvexHttpClient(convexUrl);
return Effect.tryPromise({
try: () => client.query(api.queries.entities.get, { id }),
catch: (error) => new ConvexDatabaseError('getEntity', error),
});
},
listEntities: (type: string) => {
const client = new ConvexHttpClient(convexUrl);
return Effect.tryPromise({
try: () => client.query(api.queries.entities.list, { type }),
catch: (error) => new ConvexDatabaseError('listEntities', error),
});
},
});
}
```
#### `api/src/lib/effect-handler.ts`
```typescript
import { Effect, Exit } from 'effect';
import type { Context } from 'hono';
/**
* Run Effect program in Hono route handler
* Automatically converts Effect errors to HTTP responses
*
* See effect.md for complete usage patterns
*/
export async function runEffectHandler<E, A>(
c: Context,
program: Effect.Effect<A, E>,
errorHandler?: (error: E) => { status: number; body: any }
): Promise<Response> {
const exit = await Effect.runPromiseExit(program);
return Exit.match(exit, {
onSuccess: (value) => c.json(value),
onFailure: (cause) => {
// Extract the error
const failures = cause.failures;
const error = failures.length > 0 ? failures[0] : null;
// Use custom error handler if provided
if (errorHandler && error) {
const { status, body } = errorHandler(error as E);
return c.json(body, status);
}
// Default error handling based on error tag
if (error && typeof error === 'object' && '_tag' in error) {
switch ((error as any)._tag) {
case 'UnauthorizedError':
return c.json({ error: 'Unauthorized' }, 401);
case 'NotFoundError':
return c.json({ error: 'Not found' }, 404);
case 'ValidationError':
return c.json({ error: (error as any).message }, 400);
case 'QuotaExceededError':
return c.json({
error: (error as any).message,
code: 'QUOTA_EXCEEDED'
}, 403);
case 'ConvexDatabaseError':
return c.json({
error: 'Database error',
operation: (error as any).operation
}, 500);
default:
return c.json({ error: 'Internal server error' }, 500);
}
}
return c.json({ error: 'Internal server error' }, 500);
},
});
}
/**
* Hono Context as Effect Service
*/
import { Context as EffectContext } from 'effect';
export class HonoContextService extends EffectContext.Tag('HonoContext')<
HonoContextService,
{
req: Context['req'];
env: any;
orgId?: string;
session?: any;
}
>() {}
/**
* Extract Hono context into Effect layer
*/
export const createHonoLayer = (c: Context) => {
return Layer.succeed(HonoContextService, {
req: c.req,
env: c.env,
orgId: c.get('orgId'),
session: c.get('session'),
});
};
```
### 7. Business Logic with Effect.ts
#### `api/src/services/token.ts`
```typescript
import { Effect } from 'effect';
import { ConvexService } from './convex';
/**
* Token business logic service
* Pure Effect.ts service - no side effects
*/
// Tagged errors
export class TokenNotFoundError {
readonly _tag = 'TokenNotFoundError';
constructor(readonly tokenId: string) {}
}
export class InsufficientFundsError {
readonly _tag = 'InsufficientFundsError';
constructor(readonly required: number, readonly available: number) {}
}
export class TokenService {
constructor(private convex: ConvexService) {}
/**
* Purchase tokens with validation
*/
purchase = (args: {
userId: string;
tokenId: string;
amount: number;
}): Effect.Effect<
{ success: true; newBalance: number },
TokenNotFoundError | InsufficientFundsError
> =>
Effect.gen(this, function* () {
// Validate token exists
const token = yield* Effect.promise(() => this.convex.getEntity(args.tokenId));
if (!token || token.type !== 'token') {
return yield* Effect.fail(new TokenNotFoundError(args.tokenId));
}
// Calculate cost
const cost = args.amount * token.properties.price;
// TODO: Verify user has funds (integrate with payment provider)
// For now, assume purchase succeeds
// Execute purchase via Convex
const result = yield* Effect.promise(() =>
this.convex.purchaseTokens(args.userId, args.tokenId, args.amount)
);
return {
success: true as const,
newBalance: result.balance,
};
});
/**
* Get token balance
*/
getBalance = (args: {
userId: string;
tokenId: string;
}): Effect.Effect<number, TokenNotFoundError> =>
Effect.gen(this, function* () {
const balance = yield* Effect.promise(() =>
this.convex.getTokenBalance(args.userId, args.tokenId)
);
return balance;
});
}
```
### 8. API Routes with Effect.ts
#### `api/src/routes/tokens.ts`
```typescript
import { Hono } from 'hono';
import { Effect, Layer } from 'effect';
import { runEffectHandler, createHonoLayer, HonoContextService } from '../lib/effect-handler';
import { ConvexDatabase } from '../services/convex';
import { TokenService } from '../../../convex/services/tokens/token';
import { AuthService } from '../services/auth';
import { StripeProvider } from '../services/providers/stripe';
import { auth } from '../auth';
const app = new Hono();
/**
* GET /api/tokens/:id
* Fetch token details
*/
app.get('/:id', async (c) => {
const tokenId = c.req.param('id');
// Build Effect program
const program = Effect.gen(function* () {
const db = yield* ConvexDatabase;
// Get token entity
const token = yield* db.getEntity(tokenId);
if (!token || token.type !== 'token') {
return yield* Effect.fail(new NotFoundError('Token not found'));
}
return token;
});
// Create layer with dependencies
const layer = ConvexDatabase.fromEnv(c.env.CONVEX_URL);
// Run Effect program with automatic error handling
return runEffectHandler(c, program.pipe(Effect.provide(layer)));
});
/**
* POST /api/tokens/purchase
* Purchase tokens (complete Effect.ts flow)
*/
app.post('/purchase', async (c) => {
const { tokenId, amount } = await c.req.json();
// Build Effect program
const program = Effect.gen(function* () {
// Get dependencies
const ctx = yield* HonoContextService;
const authService = yield* AuthService;
const tokenService = yield* TokenService;
// Verify authentication
const session = yield* authService.getSession(ctx.req.raw.headers);
if (!session) {
return yield* Effect.fail(new UnauthorizedError());
}
// Check organization membership (if multi-tenant)
if (ctx.orgId) {
const isMember = yield* authService.checkOrgMembership({
userId: session.user.id,
orgId: ctx.orgId,
});
if (!isMember) {
return yield* Effect.fail(
new ForbiddenError('Not a member of this organization')
);
}
}
// Execute token purchase (Effect.ts service)
const result = yield* tokenService.purchase({
userId: session.user.id,
tokenId,
amount,
orgId: ctx.orgId,
});
// Log success
yield* Effect.logInfo('Token purchase completed', {
userId: session.user.id,
tokenId,
amount,
newBalance: result.newBalance,
});
return result;
});
// Create layer with all dependencies
const layer = Layer.mergeAll(
createHonoLayer(c),
ConvexDatabase.fromEnv(c.env.CONVEX_URL),
StripeProvider.Live,
AuthService.Default,
TokenService.Default
);
// Run Effect program (errors automatically converted to HTTP responses)
return runEffectHandler(c, program.pipe(Effect.provide(layer)));
});
/**
* GET /api/tokens/balance/:tokenId
* Get user's token balance
*/
app.get('/balance/:tokenId', async (c) => {
const tokenId = c.req.param('tokenId');
const program = Effect.gen(function* () {
const ctx = yield* HonoContextService;
const authService = yield* AuthService;
const tokenService = yield* TokenService;
// Verify authentication
const session = yield* authService.getSession(ctx.req.raw.headers);
if (!session) {
return yield* Effect.fail(new UnauthorizedError());
}
// Get balance
const balance = yield* tokenService.getBalance({
userId: session.user.id,
tokenId,
orgId: ctx.orgId,
});
return { balance };
});
const layer = Layer.mergeAll(
createHonoLayer(c),
ConvexDatabase.fromEnv(c.env.CONVEX_URL),
AuthService.Default,
TokenService.Default
);
return runEffectHandler(c, program.pipe(Effect.provide(layer)));
});
export const tokenRoutes = app;
/**
* Tagged Errors
*/
class NotFoundError {
readonly _tag = 'NotFoundError';
constructor(readonly message: string) {}
}
class UnauthorizedError {
readonly _tag = 'UnauthorizedError';
}
class ForbiddenError {
readonly _tag = 'ForbiddenError';
constructor(readonly message: string) {}
}
```
**Key Benefits of This Pattern:**
1. **Type-Safe Error Handling** - All errors are typed with `_tag`
2. **Automatic HTTP Conversion** - `runEffectHandler` converts Effect errors to HTTP responses
3. **Composable Logic** - Effect programs compose cleanly
4. **Testable** - Easy to test with mock layers
5. **Observable** - Built-in logging via `Effect.logInfo`
For complete Effect.ts patterns and examples, see **[effect.md](../connections/effect.md)**.
### 9. Astro Frontend Integration
#### `src/lib/api.ts` (API Client)
```typescript
/**
* API client for Hono backend
* Provides typed methods for all API endpoints
*/
export class APIClient {
private baseURL: string;
constructor(baseURL: string = import.meta.env.PUBLIC_API_URL) {
this.baseURL = baseURL;
}
private async request(
path: string,
options: RequestInit = {}
): Promise<Response> {
return fetch(`${this.baseURL}${path}`, {
...options,
credentials: 'include', // Important for Better Auth cookies
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
}
// Auth methods (Better Auth endpoints)
async signIn(email: string, password: string) {
return this.request('/api/auth/sign-in', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
}
async signUp(email: string, password: string, name: string) {
return this.request('/api/auth/sign-up', {
method: 'POST',
body: JSON.stringify({ email, password, name }),
});
}
async signOut() {
return this.request('/api/auth/sign-out', { method: 'POST' });
}
async getSession() {
const res = await this.request('/api/auth/session');
return res.json();
}
// Token methods
async getToken(id: string) {
const res = await this.request(`/api/tokens/${id}`);
return res.json();
}
async purchaseTokens(tokenId: string, amount: number) {
const res = await this.request('/api/tokens/purchase', {
method: 'POST',
body: JSON.stringify({ tokenId, amount }),
});
return res.json();
}
async getTokenBalance(tokenId: string) {
const res = await this.request(`/api/tokens/balance/${tokenId}`);
return res.json();
}
// Agent methods
async createAgent(config: any) {
const res = await this.request('/api/agents', {
method: 'POST',
body: JSON.stringify(config),
});
return res.json();
}
async getAgent(id: string) {
const res = await this.request(`/api/agents/${id}`);
return res.json();
}
// Content methods
async createContent(content: any) {
const res = await this.request('/api/content', {
method: 'POST',
body: JSON.stringify(content),
});
return res.json();
}
async listContent() {
const res = await this.request('/api/content');
return res.json();
}
}
export const api = new APIClient();
```
### 10. Dual Integration Pattern (Hono + Convex + Effect.ts)
#### `src/components/features/tokens/TokenPurchase.tsx`
```typescript
import { useState } from 'react';
import { Effect } from 'effect';
import { useQuery } from 'convex/react';
import { api as convexApi } from '@/convex/_generated/api'; // Convex hooks
import { TokenClientService } from '@/lib/effects/token-client';
import { ClientLayer } from '@/lib/effects/layers';
import { Button } from '@/components/ui/button';
import { useToast } from '@/hooks/use-toast';
import type { Id } from '@/convex/_generated/dataModel';
/**
* Token purchase component demonstrating triple integration:
* 1. Convex hooks for real-time data (live balance updates)
* 2. Effect.ts client service for API calls (type-safe errors)
* 3. Hono API backend for business logic (auth, validation, payment)
*
* See effect.md for complete Effect.ts client patterns
*/
export function TokenPurchase({ tokenId }: { tokenId: Id<'entities'> }) {
const { toast } = useToast();
const [loading, setLoading] = useState(false);
// Real-time token data via Convex (live updates!)
const token = useQuery(convexApi.queries.tokens.get, { id: tokenId });
/**
* Purchase handler using Effect.ts client service
* Provides type-safe error handling and automatic retry
*/
const handlePurchase = async (amount: number) => {
setLoading(true);
// Build Effect program
const program = Effect.gen(function* () {
const tokenService = yield* TokenClientService;
// Purchase tokens (calls Hono API)
const result = yield* tokenService.purchase({ tokenId, amount });
return result;
});
// Run Effect program with error handling
const result = await Effect.runPromise(
program.pipe(
// Provide client dependencies
Effect.provide(ClientLayer),
// Automatic retry on network errors
Effect.retry({
times: 3,
schedule: Effect.Schedule.exponential('1 second'),
}),
// Timeout after 30 seconds
Effect.timeout('30 seconds'),
// Handle success
Effect.tap(() =>
Effect.sync(() =>
toast({
title: 'Success!',
description: `Purchased ${amount} tokens`,
})
)
),
// Type-safe error handling
Effect.catchTag('UnauthorizedError', () =>
Effect.sync(() => {
toast({
title: 'Unauthorized',
description: 'Please sign in to purchase tokens',
variant: 'destructive',
});
window.location.href = '/signin';
return { success: false as const };
})
),
Effect.catchTag('TokenPurchaseError', (error) =>
Effect.sync(() => {
toast({
title: 'Purchase Failed',
description: error.message,
variant: 'destructive',
});
return { success: false as const };
})
),
Effect.catchTag('NetworkError', (error) =>
Effect.sync(() => {
toast({
title: 'Network Error',
description: 'Please check your connection and try again',
variant: 'destructive',
});
return { success: false as const };
})
),
Effect.catchTag('TimeoutException', () =>
Effect.sync(() => {
toast({
title: 'Request Timeout',
description: 'Purchase took too long, please try again',
variant: 'destructive',
});
return { success: false as const };
})
)
)
);
setLoading(false);
// Convex subscription automatically updates balance in real-time!
};
if (!token) return <div>Loading...</div>;
return (
<div className="space-y-4">
<h2 className="text-2xl font-bold">{token.name}</h2>
<p className="text-muted-foreground">
Price: ${token.properties.price}
</p>
{/* Real-time balance via Convex subscription */}
<p className="text-sm text-muted-foreground">
Your balance: {token.properties.balance || 0} tokens
</p>
<Button onClick={() => handlePurchase(100)} disabled={loading}>
{loading ? 'Processing...' : 'Buy 100 Tokens'}
</Button>
</div>
);
}
```
**Key Benefits of Triple Integration:**
1. **Real-Time Data** - Convex hooks provide live subscriptions (balance updates automatically)
2. **Type-Safe Errors** - Effect.ts client catches all error types with `catchTag`
3. **Automatic Retry** - Effect retries network failures with exponential backoff
4. **Timeout Protection** - Effect prevents hanging requests
5. **Composable Logic** - Effect programs combine cleanly
6. **Better UX** - User sees optimistic updates + error messages
For complete Effect.ts client patterns, see **[effect.md](../connections/effect.md)**.
### 11. Environment Variables
#### Hono API (`.dev.vars` / Cloudflare Workers settings)
```bash
# Better Auth
BETTER_AUTH_URL=http://localhost:8787
BETTER_AUTH_SECRET=your-secret-key
# Convex
CONVEX_URL=https://xxx.convex.cloud
CONVEX_DEPLOY_KEY=xxx
# OAuth (optional)
GITHUB_CLIENT_ID=xxx
GITHUB_CLIENT_SECRET=xxx
GOOGLE_CLIENT_ID=xxx
GOOGLE_CLIENT_SECRET=xxx
```
#### Astro Frontend (`.env`)
```bash
# Public URLs
PUBLIC_API_URL=http://localhost:8787
PUBLIC_CONVEX_URL=https://xxx.convex.cloud
```
## Development Workflow
### Terminal Setup
```bash
# Terminal 1: Hono API
cd api
bun run dev # Runs on localhost:8787
# Terminal 2: Convex backend
npx convex dev # Runs Convex functions locally
# Terminal 3: Astro frontend
cd ..
bun run dev # Runs on localhost:4321
```
### "Vibe Code" Workflow
```
1. User designs UI mockup
2. User writes React component with Convex hooks
3. Component shows real-time data (no backend changes!)
4. User deploys frontend to Cloudflare Pages
5. Done! ✅
```
**Example: Building a dashboard page**
```astro
---
// src/pages/dashboard.astro
import CreatorStats from '@/components/dashboard/CreatorStats';
---
<Layout>
<div class="container py-8">
<h1>Dashboard</h1>
{/* Real-time stats with Convex hooks - zero backend code! */}
<CreatorStats client:load />
</div>
</Layout>
```
```typescript
// src/components/dashboard/CreatorStats.tsx
import { useQuery } from 'convex/react';
import { api } from '@/convex/_generated/api';
export function CreatorStats() {
// Real-time data - updates automatically!
const stats = useQuery(api.queries.dashboard.getStats);
return (
<div className="grid grid-cols-3 gap-4">
<Card>
<CardTitle>Revenue</CardTitle>
<CardContent>${stats?.revenue || 0}</CardContent>
</Card>
{/* More cards... */}