-
-
The backlog holds unprioritized work, with one-click promotion to the active board.
-
Org members with per-organization roles. Usernames are unique within an org and reusable across orgs, enforced by a compound database index.
-
@mentions and assignments become real notifications, resolved per-organization rather than against a global user.
-
Drag-and-drop Todo → In Progress → Blocked → Review → Done. Blocked is derived, so ENG-107 returns to Todo when unblocked.
-
Shows Task details: status,priority,story points,reporter,assignee. hashtags in the description is parsed automatically into structured tags
-
The landing page. "Explore Sandbox" opens a complete, working workspace — no account, no signup wall.
-
Every create, move, block, delete and restore is written to an append-only audit trail. Who did what, and when.
Inspiration
Every team I've worked on runs the same broken relay: the board lives in one tool, the "why" lives in a chat thread, the decision lives in someone's DMs, and the audit trail lives nowhere. By the time you've reconstructed why a ticket moved, the sprint is over.
The part that bothered me the most, was blocked work. Every tracker I tried treats "blocked" as just another column. So you drag a card into Blocked and immediately lose the thing you actually needed to know: what state was it in before, and who is it waiting on? The card sits there, statusless, until someone remembers to ask in standup.
Planora OS started as a fix for that one problem and grew into a full workspace around it.
What it does
Planora OS is a multi-organization task platform where the audit trail is a first-class feature rather than an afterthought.
- Kanban board with drag-and-drop across
Backlog → Todo → In Progress → Blocked → Review → Done - Blocking that remembers — mark a task blocked by a specific person or team, and it keeps its underlying status
- Org-scoped identity — you're
@malayin one org and@mdin another, enforced at the database level - Mentions and notifications —
@usernamein a description creates a real notification for that member - Hashtag tagging —
#backendin a description is parsed into structured tags automatically - Full action log — every create, move, block, delete, and restore is written to an append-only feed
- Soft delete with correct restore — trash and archive return a task to the column it left, not to a default
- Sandbox mode — the entire product is explorable with zero signup
The sandbox (the feature I am proudest of)
Hackathon judges have minutes, not patience. A signup wall between a judge and your demo is a self-inflicted wound.
So /sandbox boots a complete, fully interactive workspace — seeded board, teammates, activity feed — with no account required. It's not a video or a screenshot tour; you can drag cards, block them, and watch the log update.
The design constraint was that it must never touch the real database. Sandbox state lives entirely in a React context provider:
export function SandboxProvider({ children, currentUserId }: SandboxProviderProps) {
const [columns, setColumns] = useState(MOCK_COLUMNS);
const [logs, setLogs] = useState<SandboxLogEntry[]>([...]);
const addLog = (action: string, details: string) => {
setLogs(prev => [{ id: `log-${Date.now()}`, action, details,
timestamp: new Date(), user: currentUser }, ...prev]);
};
// ...
}
Board components are swapped for Sandbox* wrappers that share the exact same UI but write to context instead of MongoDB. One component tree, two persistence backends.
How I built it
Next.js 16 App Router + React 19, with mutations as Server Actions — no REST layer, no separate API routes. app/actions/ holds nine action modules (task, org, auth, notification, log, comment, project, user, demo) that are the entire backend.
MongoDB + Mongoose across eight models. The one I spent longest on was OrgMembership, the join table that makes org-scoped usernames possible:
OrgMembershipSchema.index({ org_id: 1, org_username: 1 }, { unique: true });
OrgMembershipSchema.index({ org_id: 1, user_id: 1 }, { unique: true });
Two compound indexes: one guarantees usernames are unique within an org but reusable across orgs, the other stops double-joins. Pushing that invariant into the index instead of application code meant I never had to trust a race-prone read-then-write.
Auth is custom — bcrypt hashing, jose for HS256 JWTs, session in an HTTP-only cookie. Signup provisions a user, a permanent default org, an OrgMembership, and a starter General project in one flow.
UI is Tailwind v4 + shadcn/ui over Radix primitives, @hello-pangea/dnd for drag-and-drop, Framer Motion for transitions, and React Three Fiber for the animated 3D geometry on the landing page.
Challenges I ran into
Blocked isn't a status. Our first version made Blocked a real value in the status enum. It worked until you unblocked something — the task had forgotten where it came from and defaulted to Todo. The fix was to stop treating blocked as a location and start treating it as a flag that sits orthogonal to status:
if (newStatus === 'Blocked') {
task.is_blocked = true; // status is deliberately untouched
} else {
task.is_blocked = false;
task.blocked_by = undefined;
task.status = newStatus;
}
The board then reassembles Blocked as a derived column at read time by partitioning on the flag. A blocked card is still a Todo card underneath, so unblocking restores it exactly. Same trick powers trash: deleted_from_status records the origin column so restore is lossless.
Ordering. Each task carries an integer order set to its drop index, so a reorder is \( O(1) \) to write. The tradeoff I knowingly accepted is that neighbours can collide on the same index — a fractional rank, where a card dropped between neighbours \( a \) and \( b \) takes
$$\text{order} = \frac{a + b}{2}$$
would fix it without renumbering the column. It's the first thing on our list after the hackathon.
Mentions across a rename. Notifications resolve @username against OrgMembership, not the global user record — so mentions stay correct per-organization instead of leaking a person's identity from one workspace into another.
What I learned
- Put invariants in the database. The compound-index approach eliminated a whole category of bug I'd otherwise have written defensively around.
- Model state, not screens. Every bug in the blocked/trash flows traced back to the same mistake: encoding a view (a column) as state (a status). Once I separated derived views from stored state, both features got smaller and stopped breaking.
- Server Actions genuinely collapse the stack. Deleting the API layer removed the request/response plumbing where most of our early bugs lived.
- Remove the wall in front of your demo. Building sandbox mode took a day and did more for the project than any feature behind the login.
What's next for Planora OS
Fractional ordering, real-time multiplayer via MongoDB change streams, sprint cycles with burndown from the story points I already store, MCP integration to let AI take over tasks/help in going through them, and a keyboard-first command palette (cmdk is wired up, unused).


Log in or sign up for Devpost to join the conversation.