DEV Community

Cover image for CreatorDesk: a self-hosted approval loop for commission artists
Johnny Santamaria
Johnny Santamaria

Posted on

CreatorDesk: a self-hosted approval loop for commission artists

Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle Submission 💸

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle

What I Built

CreatorDesk is a self-hosted, open-source tool that solves one specific problem for independent digital artists: tracking which version of a commission a client approved, and why.

An artist uploads a work-in-progress (a sketch, a lineart, a colored render), sends the client a private link, and the client clicks Approve or Request changes with optional written feedback. Every revision and every decision is recorded with a timestamp. The client never creates an account. The artist never loses the conversation to a DM thread.

Who would pay for it: a working commission artist who does 4–15 paid commissions a month, whose income depends on delivering the right version, and who currently tracks client feedback in Discord DMs, Trello boards, and a spreadsheet. CreatorDesk replaces that with one page the client can actually use and one history the artist can actually point to when scope creep happens.

The problem it solves is narrow on purpose: the approval loop, not the whole business. Not a marketplace, not a portfolio host, not a CRM, not payments. The wedge is the moment between "here is the sketch" and "yes, that's right" — because that's where commission failures happen, and where artists have the least infrastructure.

Demo

The app runs on localhost:3000 after four commands:

git clone https://github.com/johnny603/CreatorDesk
cd CreatorDesk && npm install
npm run db:reset     # applies migrations, seeds a demo artist
npm run dev
Enter fullscreen mode Exit fullscreen mode

A full loop — create commission → upload sketch → send link → client requests change → upload v2 → client approves → mark delivered — takes about two minutes and is documented step-by-step in the README.

Screenshot: artist dashboard, mid-demo.

CreatorDesk
+ New commission

Title              Client   Status                      Updated
● Test commission  Alex     needs your next revision    2026-10-18T01:26:48Z
  Test 2           Joe      waiting on client           2026-10-18T01:25:40Z
  Test 3           Sam      approved — ready to deliver 2026-10-18T01:20:49Z
Enter fullscreen mode Exit fullscreen mode

The dot (●) marks commissions where the artist has a next action — in_progress (client requested changes) and approved (client signed off, ready to deliver). The dashboard sorts by actionability, not recency, so the answer to "what do I do next?" is the top row.

Screenshot: client approval page.

Test commission
Version 2
[image of the sketch]

"hair updated — changed the shape per your note"

Feedback (optional): [textarea]

[Approve]   [Request changes]

▸ Revision history (2 versions)
Enter fullscreen mode Exit fullscreen mode

The <details> expansion shows v1 ("you asked for changes: 'make the hair blue' (2026-10-18T01:20:33Z)") and v2 ("currently reviewing"). The client can see what they asked for. The artist can point to it later.

Code

GitHub: github.com/johnny603/CreatorDesk — tagged v0.1.1, MIT licensed, 32 passing tests.

How I Built It

The stack is deliberately boring:

Layer Technology Purpose
App framework Next.js 14 (App Router) + TypeScript Server-rendered routes; server actions for mutations
UI React, minimal client JS Forms work without hydration where possible
Database SQLite via better-sqlite3 Single-file state; the whole app is data/
Data access Raw SQL in a small module No ORM; every query is visible
Uploads Local filesystem, sha256-named Duplicate uploads are free; no cloud storage
Client auth Random 256-bit token, sha256-stored The token in the URL is the entire client session
Artist session Single-user stub (session.ts) Real auth is deferred until validation

Where the open pieces matter — and where they are the reason the project works:

The case for open-source here is not "we used an LLM." It is the same case the challenge asks about: does the open approach let you do something a closed one can't?

Three specific answers, all structural:

  1. Data control. A commission artist's work-in-progress is often under NDA, embargo, or just the artist's own embargo until the client has paid. Sending those images through a third-party SaaS means the artist is trusting a company's retention policy with their client's intellectual property. CreatorDesk is self-hosted: data/creatordesk.sqlite and data/uploads/ are on the artist's own machine, and the README documents exactly how to back them up. There is no server the artist doesn't control.

  2. Cost. A self-hosted SQLite app costs $0/mo to run on a $5 VPS or an old Mac Mini. HoneyBook is ~$29/mo; Dubsado is more. For a part-time artist doing 3 commissions a month, the closed tools' pricing is the single most cited reason for not adopting them. The open tool's cost is the price of the machine it runs on, which the artist already owns.

  3. Portability. The schema is a plain .sql file. If CreatorDesk is abandoned, the artist can sqlite3 data/creatordesk.sqlite and export every commission, every revision's file path, every decision, every feedback note. The data is not trapped behind an API the vendor can deprecate.

The AI piece — what it actually is, and what it is not:

This is not an "AI features" project. CreatorDesk does not call an LLM at runtime, and the artist is not writing prompts. The role of open AI tooling is in the build, not the runtime: the project was planned, reviewed, and debugged in Claude Code over a multi-day session, with open-source design patterns (Facade applied lightly, State as an explicit transition table, everything else deferred) as the reference. The repository's commit history and this blog post's companion agent session are the record of that.

That distinction matters for the challenge's framing. The challenge asks for a project where open pieces are what makes it work — not for every project to ship a model. Here, the open-source ethos (self-host, portability, no vendor lock-in) is what makes the product's value proposition possible in the first place. A closed-API version of this tool would defeat its own pitch.

The system diagram:

┌──────────────────────┐         ┌────────────────────────┐
│ Client (browser)     │         │ Artist (browser)       │
│ no account           │         │ single-user session    │
└──────────┬───────────┘         └───────────┬────────────┘
           │ token in URL                    │ cookie
           ▼                                 ▼
┌──────────────────────────────────────────────────────────┐
│ Next.js routes (src/app)                                 │
│  /a/[token]              client approval page            │
│  /a/[token]/decision     POST approve / request changes  │
│  /c/[commissionId]       artist detail + upload          │
│  /c/[commissionId]/actions server actions                │
│  /api/commissions        create commission               │
│  /api/files/[filename]   file serving, token-scoped      │
└──────────┬───────────────────────────────────────────────┘
           │
           ▼
┌──────────────────────────────────────────────────────────┐
│ lib (src/lib)                                            │
│  commissions.ts    workflow rules + token lookup         │
│  revisions.ts      the atomic decision claim             │
│  approval-tokens.ts  security boundary                   │
│  uploads.ts        sha256-named file storage             │
│  db.ts             connection + transaction wrapper      │
└──────────┬───────────────────────────────────────────────┘
           │
           ▼
┌──────────────────────────────────────────────────────────┐
│ data/                                                    │
│  creatordesk.sqlite (artists, commissions, revisions)    │
│  uploads/<sha256>.<ext>                                  │
└──────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The workflow, start to finish — the client approval loop:

  1. Artist creates a commission (/c/new) → POSTs to /api/commissions. commissions.create() generates a 256-bit random token, stores sha256(token) and a 30-day expiry, and returns the raw token once. The artist sees a "Commission created" page with a copy button.
  2. Artist uploads a revision on /c/[commissionId]. revisions.submitRevision() inserts a row with version_number = 1, decision = 'pending', and updates the commission to awaiting_approval, in a single transaction.
  3. Artist sends the link through their own channel (email, DM, anywhere). CreatorDesk does not send email.
  4. Client opens /a/[token]. approval-tokens.hashToken(token) + commissions.findByTokenHash(hash) resolves the token to exactly one commission (or returns undefined if expired). If the latest revision is pending, the page renders the image, the artist's note, two buttons, and a collapsed revision history. If not, it renders a status-specific message ("The artist is preparing your first revision" / "You approved the latest version" / etc.).
  5. Client clicks Approve. POST to /a/[token]/decision. revisions.recordDecision(revisionId, decision, feedback) runs a single SQL transaction:
    • Claims the revision: UPDATE revisions SET decision = ? WHERE id = ? AND decision = 'pending' AND version_number = (SELECT MAX(version_number) ...) AND EXISTS (SELECT 1 FROM commissions WHERE ... status = 'awaiting_approval')
    • If zero rows updated: classifies the failure (already_decided / not_latest / wrong_state / not_found) and returns it to the route, which returns a specific message.
    • If one row updated: also updates the commission's status to approved or in_progress, inside the same transaction.
  6. Client sees "Thanks — your feedback is recorded" (/a/[token]?thanks=1). The artist's dashboard now shows the decision.
  7. Artist uploads v2 (or clicks Mark delivered if v2 was approved).
  8. The history is retained. Every version, decision, and feedback note stays in the revisions table forever. The client page shows it collapsed; the artist page shows it fully.

The transaction at step 5 is the load-bearing part. All four race scenarios the project tests (already_decided, not_latest, wrong_state, not_found) are about what happens when two of those steps overlap — a stale tab, a duplicate click, an artist uploading while the client is deciding. The SQL is one statement, and its WHERE clause is the entire concurrency control.

Why Does Open Innovation Matter?

The three reasons from above, restated because the challenge asks directly:

Client work is confidential by default. A sketch under commission is not public. An artist's client may be a small business commissioning packaging art, an author commissioning a book cover, a family commissioning a portrait. None of them expect their WIP to leave the artist's machine. A self-hosted tool makes that a property, not a promise. A closed-API version of this app would need to explain its retention policy in every client email, which is a conversation neither party wants.

A working artist's tooling budget is near zero. The $29/mo tools are not priced wrong — they're priced for the agency freelancers who are their market. A part-time illustrator doing 3 commissions a month cannot amortize a $29/mo subscription over a $150 commission. The self-hosted version costs the artist nothing they aren't already paying for. That is not a marketing angle; it is the difference between the tool existing and not existing for that customer.

The schema is the product. migrations/001_init.sql is 58 lines. Anyone can read it in a minute and understand every state a commission can be in. That's the open-source contract: if I disappear, the data is yours, and the shape of it is legible. A closed SaaS's schema is invisible; a self-hosted OSS schema is a .sql file in a git repo. For a tool holding a person's client history, legibility is a feature.

My Agent Session

The build was done in Claude Code across several sessions. The agent session includes: product direction research, the four design review rounds on the transaction contract, the two test-suite bug fixes (classifyFailure ordering and the submitRevision guard), the two next build type errors (BodyInit on Buffer, the DecisionResult conditional-type collapse), and the UX round that added the thanks page and contextual "nothing to review" messages.

The pattern worth naming from that session: every design-level problem was caught in review, and every implementation bug was caught by npm test or npm run build, not review. That's a useful distinction for anyone considering this workflow — review catches design, tests catch code, and neither substitutes for the other.

(Agent session embed/link per challenge instructions.)

My First User

The honest state: I tested the full loop myself end-to-end on v0.1.1 — create commission → upload → approve → request changes → upload v2 → approve → mark delivered — with both single and bulk delete verified. That's not a real first user; it's the demo working.

What I did not do, and what the project's own research plan gates: five interviews with working commission artists before building more. The README's "What's next" section says this explicitly:

The next milestone is user validation, not more features. Five interviews with working commission artists, then the answer to: "is the approval loop actually painful enough that artists will switch for it?"

The first real user will come from that round. The tool is ready for them: git clone, four commands, a two-minute demo loop, a README that documents what it does and does not do. The scope is deliberately bounded so that the first user's feedback goes to the right place — the next feature, not a bug report about the demo.

What a contributor, mentor, or early user can help with right now:

Read src/lib/revisions.ts and tell me whether the transaction claim reads correctly to you. If you have written a conditional UPDATE … WHERE … before, you will spot a problem faster than I will.

Run the demo on your machine and tell me where you got stuck. The README assumes Node 20, better-sqlite3 prebuilds, and a browser. Every deviation from that assumption is a bug report I can't write myself.

If you are a commission artist: the 15-minute interview script is in the README. If you are not, a referral to one is as useful as a review.

The tool works. What I need next is not more code — it is the data, the review, and the restoration test that tell me whether the thing I built is solving the problem I think it is. That's the honest state of the project, and it's what the next milestone is for.

Footnote

Open Source (primary). This is a self-hosted, MIT-licensed project where the open-source property — data ownership, legibility, cost — is the product's value proposition, not an incidental choice. No partner category claimed.


Top comments (0)