cd /news/ai-tools/creatordesk-a-self-hosted-approval-l… Β· home β€Ί topics β€Ί ai-tools β€Ί article
[ARTICLE Β· art-152167] src=dev.to β†— pub= topic=ai-tools verified=true sentiment=↑ positive

CreatorDesk: a self-hosted approval loop for commission artists

A developer built CreatorDesk, a self-hosted, MIT-licensed open-source tool that tracks which version of a commission a client approved and why. Artists upload work-in-progress images, send clients a private tokenized link, and clients approve or request changes without creating an account, with every revision and decision timestamped. The Next.js 14 and SQLite app ships as v0.1.1 with 32 passing tests and runs locally after four commands.

by read10 min views2 publishedOct 18, 2026

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

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.

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

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

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)

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.

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

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:

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.

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.

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>                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The workflow, start to finish β€” the client approval loop:

/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./c/[commissionId]. revisions.submitRevision() inserts a row with version_number = 1, decision = 'pending', and updates the commission to awaiting_approval, in a single transaction./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.)./a/[token]/decision. revisions.recordDecision(revisionId, decision, feedback) runs a single SQL transaction: 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') already_decided / not_latest / wrong_state / not_found) and returns it to the route, which returns a specific message.approved or in_progress, inside the same transaction./a/[token]?thanks=1). The artist's dashboard now shows the decision. 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 up while the client is deciding. The SQL is one statement, and its WHERE clause is the entire concurrency control.

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.

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.)

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.

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.

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.

── more in #ai-tools 4 stories Β· sorted by recency
── more on @creatordesk 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/creatordesk-a-self-h…] indexed:0 read:10min 2026-10-18 Β· β€”