I've been building full-stack applications for years. Across every project — whether it was a SaaS product, an internal tool, or a side project — the database schema phase always felt the same: open a text editor, write some SQL or ORM definitions, squint at the relationships in your head, and hope you got the data model right before writing a thousand lines of application code on top of it.
The tooling around schema design hasn't meaningfully changed in twenty years. You've got pgAdmin, MySQL Workbench, a handful of web-based ERD tools that export PNGs, and the ORM file in your repo. None of them give you what you actually need: a visual, collaborative, version-controlled environment where the schema is a living artifact — not a static diagram that's out of date by the time you screenshot it.
So I built PicaDeck.
PicaDeck in one sentence
A visual database schema editor with Git-like version control — branches, commits, pull requests, and conflict detection — built for teams who want to design their data model the way they design their code: collaboratively and with a full audit trail.
The problem: schema design is still a solo activity in a text file
Think about how the rest of your stack has evolved. UI design went from Photoshop to Figma — real-time, multiplayer, in the browser. Infrastructure went from manually SSHing into servers to declarative IaC with Terraform and Pulumi. API contracts moved from wiki pages to versioned OpenAPI specs with generated clients.
Database schemas? Still a migration folder with 300 files and a prayer. The "visual" option is a diagramming tool that requires you to manually redraw the schema every time it changes. The "collaborative" option is pasting a screenshot in Slack and asking "does this look right?"
Here's what specifically breaks down at scale:
- 1No single source of truth. The ORM says one thing, the migration folder says another, and the actual database might have manual ALTER statements that nobody committed.
- 2No safe way to experiment. Want to try a different normalization approach? You have to mentally simulate it or set up a whole new migration branch — and migration branches don't merge well.
- 3No review process for schema changes. Code gets pull requests. Infrastructure gets plan diffs. Schema changes get… a migration file in a PR that most reviewers skip because it's auto-generated.
- 4Schemas are graphs, but we treat them like text. A database schema is fundamentally a graph — tables are nodes, foreign keys are edges. Reading it as sequential text is like navigating a city by reading its street directory instead of looking at a map.
What PicaDeck actually is
PicaDeck is a web application where you design, visualize, and manage your database schema on an interactive canvas. Tables are draggable nodes. Relationships are visible edges. Columns, types, constraints, and indexes are configured through a property panel. The entire schema state is persisted and version-controlled.
Visual canvas
Built on React Flow. Tables are nodes with columns listed inside. Drag to rearrange, zoom to navigate, click to edit. Relationships render as animated edges between foreign key pairs.
Git-like workflow
Create branches, make changes in isolation, commit with messages, open pull requests, detect conflicts, merge safely. Full version history with immutable snapshots.
Multi-database support
PostgreSQL, MySQL, and MongoDB from a single interface. Each engine has its own type system, constraints, and validation rules — PicaDeck handles the differences automatically.
Export to real code
Generate Prisma schemas, TypeORM entities, Drizzle configs, raw SQL DDL, and more. The visual model is the source of truth; the code is the output artifact.
Under the hood: the technical architecture
Since this is going on Hacker News, let me talk about how it's actually built. PicaDeck is a Next.js application with React 19, TypeScript, and Tailwind CSS. The schema canvas is powered by React Flow. State management uses Redux Toolkit with a custom undo/redo system built on Immer patches.
Canvas rendering with React Flow
Each table is a custom React Flow node — a TableNode component that renders the table name, all columns with their types and constraints, and connection handles for relationships. Relationships are custom edge components (ConnectionEdge) that draw animated SVG paths between the source and target columns.
The canvas supports pan, zoom, auto-layout (using a force-directed algorithm), table categories with color coding, and a minimap for navigation. Every canvas interaction — moving a table, zooming in, selecting a node — is decoupled from the schema data layer. Moving a table changes position state; it doesn't dirty the schema.
Undo/redo with Immer patches
One of the trickier engineering problems was implementing undo/redo for a complex nested state tree. The schema state includes tables, columns, relationships, enum types, composite types, and categories — all with cross-references. A naive "snapshot the entire state on every action" approach would burn through memory quickly.
Instead, we use Immer's produceWithPatches API. Every data-modifying Redux action produces a set of granular JSON patches (RFC 6902) and their inverses. Undo applies the inverse patches; redo re-applies the forward patches. A custom deepMutate helper walks Immer drafts recursively to produce the most granular patches possible, so undoing "add a column" doesn't replay the entire table state — it removes exactly that column.
// Every schema mutation produces patches
const [nextState, patches, inversePatches] = produceWithPatches(
currentState,
(draft) => {
// e.g., add a column to a table
draft.tables[tableId].columns.push(newColumn);
}
);
// Push to history stack
undoStack.push({ patches, inversePatches });
// Undo: apply inverse patches
const undoneState = applyPatches(currentState, inversePatches);
// Redo: re-apply forward patches
const redoneState = applyPatches(undoneState, patches);Auto-save with zero data loss
PicaDeck has two persistence tiers. On the free plan, every change writes directly to the database tables — immediate, always persisted. On paid plans with Git mode enabled, changes write to a draftSchema JSON field on the branch record. This draft is auto-saved with a 2-second debounce.
The interesting edge case is tab closure. If a user closes their browser tab mid-edit, a normal fetch() request would be cancelled by the browser. We handle this with the navigator.sendBeacon() API in a beforeunload handler. Beacon requests are fire-and-forget — the browser guarantees delivery even during page teardown. On the server side, a dedicated POST /api/schema/save-draft endpoint handles these fast saves with minimal overhead.
useEffect(() => {
const handleBeforeUnload = () => {
if (!hasDraft || !schemaData) return;
// sendBeacon survives page teardown
navigator.sendBeacon(
"/api/schema/save-draft",
JSON.stringify({
branchId,
schemaData,
})
);
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [hasDraft, branchId, schemaData]);Branching and commits: Git semantics for schemas
PicaDeck's branching model mirrors Git's mental model but operates on structured schema data rather than text files. When you create a branch, it forks from the current committed state of the parent branch. Changes on the new branch are isolated — other team members working on other branches don't see your drafts.
A commit promotes the draft to permanent storage: it writes the schema data to the relational tables (SchemaTable, Column, Relationship), creates an immutable snapshot in Commit.schemaSnapshot, and clears the draft. Every commit is a full snapshot — not a diff. This means you can always see exactly what the schema looked like at any point in time, and reverts are trivial: just restore the snapshot.
Conflict detection at the entity level
This is the part I'm most proud of. When you open a pull request to merge one branch into another, PicaDeck doesn't just compare "did both branches change?" — it compares at the entity level. Did both branches modify the same table's description? The same column's data type? The same relationship's cascade behavior?
Conflicts are categorized by type: TABLE, COLUMN, RELATIONSHIP, and SCHEMA_TYPE (enums and composite types). Each conflict records the source value, target value, and a human-readable description. Merges are blocked until all conflicts are resolved.
Resolution strategies are: accept theirs (source branch wins), keep ours (target branch wins), or manual (pick and choose per conflict). This is roughly analogous to Git's merge strategies, but operating on structured data instead of line-level text diffs.
function detectSchemaConflicts(sourceBranch, targetBranch) {
const conflicts = [];
for (const sourceTable of sourceBranch.tables) {
const targetTable = targetBranch.tables
.find(t => t.name === sourceTable.name);
if (!targetTable) continue;
// Check table-level properties
if (sourceTable.description !== targetTable.description) {
conflicts.push({
type: "TABLE",
entityName: sourceTable.name,
sourceValue: sourceTable.description,
targetValue: targetTable.description,
});
}
// Check column-level changes
for (const sourceCol of sourceTable.columns) {
const targetCol = targetTable.columns
.find(c => c.name === sourceCol.name);
if (!targetCol) continue;
if (sourceCol.dataType !== targetCol.dataType ||
sourceCol.nullable !== targetCol.nullable) {
conflicts.push({
type: "COLUMN",
entityName: sourceCol.name,
tableName: sourceTable.name,
sourceValue: sourceCol,
targetValue: targetCol,
});
}
}
}
return conflicts;
}How PicaDeck compares to existing tools
I've used most of the tools in this space, so let me be specific about where PicaDeck sits.
| Feature | dbdiagram.io | DrawSQL | pgAdmin / Workbench | PicaDeck |
|---|---|---|---|---|
| Visual editor | Text-based DSL | Visual | Visual (desktop) | Visual (web) |
| Branching | No | No | No | Yes — full Git model |
| Commits & history | No | No | No | Yes — immutable snapshots |
| Pull requests | No | No | No | Yes — with conflict detection |
| Team collaboration | Share link | Team plans | No | Orgs, roles, permissions |
| Multi-DB support | PostgreSQL, MySQL | Multiple | Single DB each | PostgreSQL, MySQL, MongoDB |
| Code export | SQL only | SQL only | SQL only | Prisma, TypeORM, Drizzle, SQL, more |
I want to be honest: dbdiagram.io is great for quick, solo schema sketches. DrawSQL has a nice UI for visual modeling. If you just need to draw a diagram and export SQL, those tools work fine. PicaDeck is for when you need more — when multiple people are evolving a schema simultaneously, when you need a review process, when you want to experiment on a branch without affecting the production schema definition, and when you want your schema to export to the ORM your project actually uses.
The tech stack and why we chose it
Some quick notes on the stack for the technically curious:
- Next.js 16 with App Router — Server components for data fetching, server actions for mutations. No separate API layer. The server action middleware pattern (
withAuthAndErrorHandling) keeps auth and error handling DRY across ~130 server actions. - Redux Toolkit + RTK Query — Redux for the schema state tree (tables, columns, relationships, canvas position, undo history). RTK Query for CRUD operations with cache invalidation. React Context for simple things like theme.
- Prisma ORM + PostgreSQL — 30 tables covering auth, multi-tenancy, schema storage, branching, commits, pull requests, billing, audit logging, and notifications. Soft deletes everywhere.
- React Flow — Powers the interactive canvas. Custom node and edge components. We tried building our own canvas renderer initially — don't do that. React Flow handles viewport transforms, hit testing, edge routing, and accessibility out of the box.
- Paddle for billing — Paddle is a Merchant of Record, which means they handle tax compliance, payment methods, and invoicing. Per-seat pricing with automatic seat adjustment when team members are added or removed.
Lessons learned building this
A few non-obvious things I learned along the way that might be useful if you're building something similar:
Snapshot-based version control is simpler than diff-based
Early on, I tried storing schema diffs between commits. The merge logic was nightmarish — you had to compose diffs, handle deletions referencing entities that might not exist, and deal with ordering. Storing full snapshots per commit uses more storage but makes every operation trivial: reverting is just restoring a snapshot, comparing is just diffing two JSON objects, and you never have a corrupted state from a failed diff application.
Auto-save needs more thought than you'd expect
A 2-second debounce sounds simple until you consider: what if the save fails? What if the network drops? What if the user closes the tab during the debounce window? The sendBeacon approach for tab closure was the key insight. For network failures, we retry with exponential backoff and keep the dirty flag set until the save succeeds.
Immer patches are underrated for undo/redo
Most undo/redo implementations either snapshot the entire state (memory intensive) or manually write inverse operations for every action (tedious and error-prone). Immer's patch system gives you the best of both: granular, automatic, and correct. The produceWithPatches API is one of the most underused features in the React ecosystem.
Don't build your own canvas renderer
I spent two weeks building a custom canvas with SVG before switching to React Flow. Those two weeks were wasted. The amount of edge cases in canvas rendering — viewport transforms, touch handling, edge routing around nodes, minimap synchronization, keyboard navigation — is enormous. Use a library. Customize the nodes and edges, not the rendering engine.
What's on the roadmap
PicaDeck is usable today, but there's a lot more we want to build:
- Real-time collaborative editing — Multiple people editing the same branch simultaneously, with live cursors and presence indicators. The architecture is already WebSocket-ready.
- Automatic migration generation — Diff two commits and generate the ALTER statements needed to migrate between them, for any supported database engine.
- CI/CD integration — A CLI tool and GitHub Action that validates schema changes in PR checks, similar to how Terraform plan works for infrastructure.
- Schema import — Point PicaDeck at an existing database connection string and reverse-engineer the visual schema automatically.
Try it
PicaDeck has a free tier that's genuinely useful — up to 3 projects, 3 team members, and full visual schema editing with auto-save. The Git workflow (branching, commits, pull requests) is available on paid plans.
There's also an interactive demo you can try without creating an account — it loads a dummy e-commerce schema on a canvas so you can see the editor in action.
I'd love feedback from the HN community. If you work with databases regularly, I'm curious: what's the biggest pain point in your current schema workflow? What would make you switch to a tool like this?
Ready to try a better way to design databases? Create your free PicaDeck account → or try the interactive demo →