Tools like Monday.com, Notion, Linear, and Airtable are powerful — but they're bloated, expensive, and built for someone else's workflow.
With Prism, you own the stack. Write the UI exactly how you want it, drop it in a folder, and it's live. Vibe code a custom project tracker, a personal CRM, or a habit tracker in an afternoon and run it yourself — no subscriptions, no vendor lock-in.
Every page is a module — a self-contained folder that the server auto-discovers on startup. No registration, no config changes.
src/modules/
│
├── 📊 dashboard/ → yoursite.com/dashboard
│ ├── index.ts → backend routes + logic
│ └── public/ → HTML, CSS, JS served automatically
│ ├── index.html
│ ├── app.js
│ └── style.css
│
├── 📋 kanban/ → yoursite.com/kanban
│ ├── index.ts
│ └── public/
│
└── 👥 crm/ → yoursite.com/crm
├── index.ts
└── public/
That's the whole pattern. Folder name becomes the route. Static files are served. APIs are scoped. Done.
git clone https://github.com/AnthonyChen05/prism.git
cd prism/backend
cp .env.example .env
docker compose up -d
# First time — run migrations
docker compose exec api npx prisma migrate dev --name initThen open http://localhost:3000
Requires PostgreSQL and Redis running locally.
cd backend
npm install
cp .env.example .env # then update DATABASE_URL and REDIS_HOST
npx prisma generate
npx prisma migrate dev --name init
npm run devA module needs one file: src/modules/<name>/index.ts
import type { AppModule } from '../../shared/types/module'
const MyModule: AppModule = {
name: 'my-module',
version: '1.0.0',
async register(server, services, prefix) {
// prefix = "/my-module" — derived from folder name
// Serve a page
server.get(prefix, { config: { public: true } } as never, async (_req, reply) => {
reply.type('text/html').send('<h1>Hello from my module</h1>')
})
// Add API routes
server.get(`${prefix}/api/data`, { config: { public: true } } as never, async () => {
return { items: await services.db.yourModel.findMany() }
})
}
}
export default MyModuleAdd a public/ folder next to it and your static files are served at /<name>-assets/.
In your HTML — use {{ASSETS}} for asset paths (replaced at serve time):
<link rel="stylesheet" href="{{ASSETS}}/style.css" />
<script src="{{ASSETS}}/app.js"></script>In your JS — use window.location.pathname as the API base:
const API = window.location.pathname.replace(/\/$/, '')
const data = await fetch(API + '/api/data').then(r => r.json())The default landing page — a personal home screen with:
| Widget | Description |
|---|---|
| Quick Links | Bookmark your most-used sites with custom icons and colors |
| To-Do List | Simple persistent tasks with drag-to-reorder |
| Google Calendar | Embed your calendar with a single URL |
| RSS Feeds | Follow any RSS/Atom feed (server-side proxy, no CORS issues) |
| Custom Background | Upload your own image, stored locally |
Every module gets access to the full service layer via dependency injection:
┌─────────────────────────────────────────────────────────┐
│ CoreServices │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────────────────┐ │
│ │ db │ │ time │ │ events │ │
│ │ (Prisma) │ │ (Luxon) │ │ (EventBus) │ │
│ └──────────┘ └──────────┘ └────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌───────────────────────────┐ │
│ │ notify │ │ timer │ │
│ │ (Notifications) │ │ (Delayed Actions) │ │
│ └──────────────────┘ └───────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ scheduler (BullMQ) │ │
│ │ Persistent job queue via Redis │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
TypeScript interface
interface CoreServices {
db: PrismaClient // PostgreSQL via Prisma — query anything
time: TimeService // Timezone-aware date/time (Luxon)
notify: NotificationService // Send real-time notifications via Socket.io
timer: TimerService // Schedule actions to fire after a delay
scheduler: Scheduler // Raw BullMQ job scheduling
events: EventBus // Pub/sub between modules
}Schedule anything to happen after a delay:
// Notify in 24 hours
await services.timer.after('daily-reminder', 86_400_000, {
type: 'notify',
payload: { userId: 'user-1', title: 'Daily check-in', body: 'How are your tasks looking?' }
})
// Emit an event to other modules in 5 seconds
await services.timer.after('sync-trigger', 5000, {
type: 'event',
event: 'crm:sync',
payload: { source: 'scheduler' }
}) ┌──────────────────────┐
│ Client / Browser │
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ Fastify + Socket.io │
│ (Port 3000) │
└──────────┬───────────┘
│
┌───────────────┼───────────────┐
│ │ │
┌────────▼──────┐ ┌─────▼─────┐ ┌───────▼───────┐
│ Dashboard │ │ Time │ │ Notifications │
│ Module │ │ Module │ │ Module │
└────────┬──────┘ └─────┬─────┘ └───────┬───────┘
│ │ │
└───────────────┼───────────────┘
│
┌──────────▼───────────┐
│ Core Services │
│ db · time · events │
│ notify · timer · sched│
└─────┬──────────┬─────┘
│ │
┌───────────▼──┐ ┌───▼───────────┐
│ PostgreSQL 16│ │ Redis 7 │
│ (Prisma) │ │ (BullMQ) │
└──────────────┘ └────────────────┘
| Layer | Technology | Purpose |
|---|---|---|
| Runtime | Node.js 20 + TypeScript | Server-side logic |
| Framework | Fastify 5 | HTTP server + routing |
| Database | PostgreSQL 16 + Prisma | Persistent storage + ORM |
| Queue | BullMQ + Redis 7 | Background jobs + scheduling |
| Realtime | Socket.io | Live push notifications |
| Auth | JWT | Route protection |
| Container | Docker + Docker Compose | One-command deployment |
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Server port |
LANDING_MODULE |
dashboard |
Module to redirect / to |
DATABASE_URL |
(see .env.example) | PostgreSQL connection string |
REDIS_HOST |
redis |
Redis hostname |
JWT_SECRET |
(change this) | JWT signing secret |
JWT_EXPIRES_IN |
15m |
Access token lifetime |
To change the landing page:
LANDING_MODULE=kanbanThings people pay monthly subscriptions for that you can vibe code in a weekend:
| Build This | Instead of Paying For |
|---|---|
| Kanban board | Trello / Linear |
| Project tracker | Monday.com / Asana |
| Personal CRM | HubSpot |
| Habit tracker | Streaks / Habitica |
| Reading list | Pocket / Instapaper |
| Budget tracker | Mint / YNAB |
| Note-taking | Notion |
| Time tracker | Toggl |
| Link board | Linktree |
| Status page | Statuspage.io |
Every one of these is a folder with an HTML file and some API routes.
prism/
├── backend/
│ ├── src/
│ │ ├── core/
│ │ │ ├── server.ts # Server bootstrap, auth, Socket.io
│ │ │ ├── plugin-loader.ts # Auto-discovers modules
│ │ │ └── services/ # db, time, notify, timer, scheduler, events
│ │ ├── modules/
│ │ │ ├── dashboard/ # Built-in personal dashboard
│ │ │ ├── time/ # Time + timer API
│ │ │ └── notifications/ # Notification history + push
│ │ └── shared/
│ │ └── types/module.ts # AppModule, CoreServices, TimerAction
│ ├── prisma/schema.prisma
│ ├── docker-compose.yml
│ ├── Dockerfile
│ └── .env.example
└── README.md
MIT — do whatever you want with it.