Architecture
Cohera is designed as a library of loosely coupled modules that developers compose to build community platforms. Each module bundles its frontend, backend, and data layers while maintaining independence between these layers.
Design Principles
Section titled “Design Principles”- Loosely coupled: Each module exposes clean APIs for independent use
- Independent layers: Use just the backend, just the UI, or the complete module
- Fully featured: Modules provide everything for the most common use case
- Coherently integrated: Modules integrate with each other
- Type safety: End-to-end type safety
- Monolithic deployment: Optimized for simple deployment
- Fine-tuned DX: We take care to make the experience delightful for developers
- Automation, not magic: Everything is transparently set up and easy to reason about
- Imperative Shell, Functional Core: Every module cleanly separates logic from side-effects
- Federation ready: Optional ActivityPub federation via core layer
Technology Stack
Section titled “Technology Stack”We built on these awesome technologies:
- Frontend: SvelteKit &
shadcn-svelte - Backend: tRPC (or experimental remote functions?)
- Database: PostgreSQL via Drizzle ORM
- Type Safety: TypeScript via tRPC & drizzle
- Federation: fedify (optional)
- Mobile: Capacitor (optional)
Integration Example
Section titled “Integration Example”Here’s how developers build a platform with Cohera:
-
Install Modules
add modules cohera module add posts profiles -
run migrations cohera db migrateThese file changes are done automatically for you when using the
coheracli.src/lib/server/db/schema.ts export * from "@cohera/posts/db";export * from "@cohera/events/db";src/lib/server/trpc.ts import { postsRouter } from "@cohera/posts/api";import { eventsRouter } from "@cohera/events/api";export const router = t.router({posts: postsRouter,events: eventsRouter,}); -
Use in Frontend
routes/+page.ts import { trpc } from "$lib/trpc/client";import type { PageLoad } from "./$types";export const load: PageLoad = async (event) => {const client = trpc(event);return {posts: await client.posts.list.query(),events: await client.events.list.query(),};};routes/+page.svelte <script lang="ts">import { PostCard } from "@cohera/posts/ui";import { EventCard } from "@cohera/events/ui";import type { PageProps } from "./$types";let { data }: PageProps = $props();</script><section><h2>Recent Posts</h2>{#each $data.posts as post (post.id)}<PostCard {post} />{/each}</section><section><h2>Upcoming Events</h2>{#each $data.events as event (event.id)}<EventCard {event} />{/each}</section> -
Deploy
deploy cohera deploy
Package Structure
Section titled “Package Structure”Each Cohera module is a single npm package with structured exports:
Example Usage
Section titled “Example Usage”// Backend: Add router to your tRPC appimport { postsRouter } from "@cohera/posts/api";
// Frontend: Use Svelte componentsimport { PostCard, PostList } from "@cohera/posts/ui";
// Database: Access table schemasimport { posts } from "@cohera/posts/db";
// Types: Import type definitionsimport type { Post, NewPost } from "@cohera/posts/types";This structure provides:
- Single version per module
- Tree-shakeable
- Clear separation
- Independence
Backend Layer
Section titled “Backend Layer”The backend layer consists of tRPC routers that handle business logic and data access.
Router Structure
Section titled “Router Structure”export const postsRouter = t.router({ list: t.procedure .input(z.object({ limit: z.number().optional() })) .query(async ({ input, ctx }) => { return ctx.db .select() .from(posts) .limit(input.limit ?? 10); }),
create: t.procedure .input(z.object({ title: z.string(), content: z.string() })) .mutation(async ({ input, ctx }) => { const post = await ctx.db.insert(posts).values(input).returning(); await ctx.federation?.announce(post); return post; }),});Key Characteristics
Section titled “Key Characteristics”- Each module provides its own tRPC router
- Routers compose into your main app’s router
- Database access via Drizzle ORM (Postgres)
- Context includes
dbinstance and optionalfederationservice - Types automatically inferred for frontend use
Independence
Section titled “Independence”You can use module routers in several ways:
- Import complete router in your tRPC app
- Use only specific procedures you need
- Build custom backend implementing the same type contracts
Frontend Layer
Section titled “Frontend Layer”The frontend layer provides Svelte components for SvelteKit applications.
Component Design
Section titled “Component Design”<script lang="ts"> import type { Post } from "@cohera/posts/types"; import * as Card from "$lib/components/ui/card/index.js"; import { Button } from "$lib/components/ui/button/index.js";
interface Props { post: Post; onDelete?: (id: Post["id"]) => void; }
const { post, onDelete }: Props = $props();</script>
<Card.Root> <Card.Header><Card.Title>{post.title}</Card.Title></Card.Header> <Card.Content> <p>{post.content}</p> </Card.Content> {#if onDelete} <Button variant="destructive" onclick={() => onDelete(post.id)}> Delete </Button> {/if}</Card.Root>Key Characteristics
Section titled “Key Characteristics”- Components are presentation-focused (data passed as props)
- Don’t directly call tRPC - parent handles data fetching
- Type-safe props using shared types from
/typesexport - Tailwind allows theming
- Work with any backend matching the type contract
Database Layer
Section titled “Database Layer”The database layer provides Drizzle schemas for PostgreSQL.
Schema Example
Section titled “Schema Example”import { pgTable, pgEnum, text, timestamp, uuid } from "drizzle-orm/pg-core";
export const visibilityEnum = pgEnum("visibility", [ "private", "local", "global",]);
export const posts = pgTable("posts", { id: uuid("id").defaultRandom().primaryKey(), title: text("title").notNull(), content: text("content").notNull(), authorId: uuid("author_id").notNull(), createdAt: timestamp("created_at", { mode: "date", precision: 3 }) .defaultNow() .notNull(), updatedAt: timestamp("updated_at", { mode: "date", precision: 3 }).$onUpdate( () => new Date(), ),
// Federation fields (optional) activityPubId: text("activitypub_id"), federatedAt: timestamp("federated_on", { mode: "date", precision: 3 }), visibility: visibilityEnum(),});
export type Post = typeof posts.$inferSelect;export type NewPost = typeof posts.$inferInsert;Key Characteristics
Section titled “Key Characteristics”- Each module exports Drizzle table schemas
- Schemas include optional federation fields
- Types inferred from schemas
- Migrations bundled with modules
- Single Postgres instance contains all module tables
Federation Layer
Section titled “Federation Layer”The @cohera/federation package provides ActivityPub integration.
Federation Service
Section titled “Federation Service”uses fedify
Key Characteristics
Section titled “Key Characteristics”- Single federation package shared by all modules
- Modules register handlers for their activity types
- Federation is optional (enabled via configuration)
- Handles ActivityPub protocol details (signing, delivery, inbox processing)
Module Integration
Section titled “Module Integration”create: t.procedure .input(z.object({ title: z.string(), content: z.string() })) .mutation(async ({ input, ctx }) => { const post = await ctx.db.insert(posts).values(input).returning();
await ctx.federation?.announce({ type: "Create", object: { type: "Note", ...post }, });
return post; });