About Featured All Work Contact

Internal Tool · Shadrachs Coffee

The Furnace

An internal command center for Shadrachs Coffee — one shared, always-current source of truth for projects, social, and approvals.

Product Design UX/UI & Design System Full-Stack Build Next.js + Supabase
3 → 1
Tools Consolidated
~5,150
Lines Shipped
v1 Live
In Daily Use
The Furnace — Projects board

Problem — Solution

From Scattered Tools to One Source of Truth

How a marketing team's trial-and-error across three tools became one always-current place to run the work.

"How might we give a small marketing team one always-current place to run projects, plan social, and clear approvals — without the overhead of a heavyweight project-management tool?"
Friction No Source of Truth A Monday and Trello trial-and-error period, plus a running Google Doc of marketing tasks. Nothing was current, and nothing was authoritative.
Solution One color-coded Projects board — always current — with inline subtask checklists, pinning, and free-text search across titles, descriptions, and subtasks.
Friction Status as Highlight Color The source Doc encoded status as a background highlight per line — and the highlights drifted off their text, so per-item status came out wrong.
Solution A fixed status mapping with an explicit "unassigned" state — everything seeded neutral and triaged by hand in the app instead of guessed.
Friction Approvals in the Margins Creative sign-off happened over scattered messages, with no record of who approved what, or when.
Solution A dedicated Approvals surface — drag-and-drop upload, approve or request changes, with reviewer notes, attribution, and timestamps saved alongside the decision.
Friction Nothing Watching the Dates No shared view of what was due or waiting — deadlines lived in someone's head or a stale document.
Solution A dashboard that doubles as the notification layer — due in 2 days, pending approvals, posts this week, and a recent-activity feed.
The Furnace — project detail slide-over
The Furnace — social calendar

Build & Architecture

Designed and Built, End to End

5
Tables, All RLS-Secured
19 policies on public, 4 on the storage bucket
40
Source Files
~5,150 lines of TypeScript, TSX, and CSS
8
Migrations, In Order
Core schema → RLS → storage → pinning → archiving
13 / 9
Active / Archived Projects
26 subtasks across 3 people, seeded from real data

Architecture

Server components fetch initial data; interaction and writes happen client-side against Supabase, with RLS enforcing access. Writes are optimistic and revert on failure, so the UI never waits on a round trip for a checkbox.

FrameworkNext.js 16 · App Router
BackendSupabase · Postgres 17
WritesOptimistic, revert on fail
SecurityRLS on every table

The Five Modules

Each replaces something the team was doing by hand.

  • 01Projects — board, subtasks, search
  • 02Social Calendar — month grid, drafts
  • 03Approvals — upload, review, attribute
  • 04Dashboard — due-soon & activity
  • 05Archive — restore or delete
The Furnace — approvals
The Furnace — dashboard

Design System

Semantic Tokens, Not Raw Hex

Light and dark are two token sets in one file — no component knows which mode it is in. Status colors are a fixed, non-editable mapping.

UnassignedWaiting on CarolineNeeds AttentionCompletedOrdered
Ink#171717
Surface#F2F2F2
Accent · Light#B2292E
Accent · Dark#BE494D
The Furnace — mobile projects view
The Furnace — mobile detail view

Under the Hood

Problems Found & Fixed

The parts that were not just typing.

Unlayered CSS defeating Tailwind

Red buttons rendered with black text in light mode. Base resets sat unlayered in globals.css — and unlayered CSS outranks any cascade layer — so button { color: inherit } beat text-accent-fg. Fixed by moving the resets into @layer base.

Two exposed RPC endpoints

PostgREST exposes anything in the public schema as a callable endpoint — including two SECURITY DEFINER trigger functions. Execute was revoked from anon and authenticated; Supabase advisors returned clean.

A stateful-regex link bug

.test() on a /g regex advances lastIndex between calls, so auto-linking pasted URLs worked only every other time. Replaced with index parity from the split.

Timezone-safe dates

date columns return YYYY-MM-DD; parsing those with new Date(str) treats them as UTC midnight and shifts the day backward west of Greenwich. All conversions go through local-time constructors.

Decisions Worth Defending

Small Calls, Real Reasons

Archiving is a timestamp, not a status

Putting "Archived" in the status dropdown would make it mutually exclusive with "Completed" and lose the record of how a project ended. archived_at is orthogonal — the project keeps its status and just leaves the board.

Pinning is a timestamp, not a boolean

Same reasoning — "when was this raised?" stays answerable, and ordering the pinned group by recency comes for free, without another migration.

Pinned rows don't get the urgency color

Position plus a red pin already signal importance. A red wash on top would make pinned rows indistinguishable from genuinely due-soon ones — and cost the signal that matters.

"Request changes" is not a red button

It is a review decision, not a destructive action, and shouldn't compete with Approve for the eye.

v1 Shipped

In the Box