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 @malay in one org and @md in another, enforced at the database level
  • Mentions and notifications — @username in a description creates a real notification for that member
  • Hashtag tagging — #backend in 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).

Built With

Share this project:

Updates

Submission history