Skip to content

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.

  • 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

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)

Here’s how developers build a platform with Cohera:

  1. Install Modules

    add modules
    cohera module add posts profiles
  2. Setup Backend and Database

    run migrations
    cohera db migrate

    These file changes are done automatically for you when using the cohera cli.

    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,
    });
  3. 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>
  4. Deploy

    deploy
    cohera deploy

Each Cohera module is a single npm package with structured exports:

  • Directory@cohera/posts/
    • api tRPC routers and procedures (backend)
    • ui Svelte components (frontend)
    • db Drizzle schemas and migrations (data)
    • types Shared TypeScript types
// Backend: Add router to your tRPC app
import { postsRouter } from "@cohera/posts/api";
// Frontend: Use Svelte components
import { PostCard, PostList } from "@cohera/posts/ui";
// Database: Access table schemas
import { posts } from "@cohera/posts/db";
// Types: Import type definitions
import type { Post, NewPost } from "@cohera/posts/types";

This structure provides:

  • Single version per module
  • Tree-shakeable
  • Clear separation
  • Independence

The backend layer consists of tRPC routers that handle business logic and data access.

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;
}),
});
  • Each module provides its own tRPC router
  • Routers compose into your main app’s router
  • Database access via Drizzle ORM (Postgres)
  • Context includes db instance and optional federation service
  • Types automatically inferred for frontend use

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

The frontend layer provides Svelte components for SvelteKit applications.

<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>
  • Components are presentation-focused (data passed as props)
  • Don’t directly call tRPC - parent handles data fetching
  • Type-safe props using shared types from /types export
  • Tailwind allows theming
  • Work with any backend matching the type contract

The database layer provides Drizzle schemas for PostgreSQL.

@cohera/posts/db/schema.ts
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;
  • 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

The @cohera/federation package provides ActivityPub integration.

uses fedify

  • 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)
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;
});