DEV Community

Cover image for I built Salt Wharf, a callboard where the later posted sheet wins
Niccolò Mascaro
Niccolò Mascaro

Posted on

I built Salt Wharf, a callboard where the later posted sheet wins

Sanity Challenge Path Two Submission

This is a submission for the Sanity Challenge, Path Two: Vibe-Code Something Strange

What I Built

I built Salt Wharf as a stage-door callboard for a fictional Valletta theatre company in tech week on The Last Luzzu. Eight people, ten scenes. The public page is meant to feel like the sheet taped beside the stage door. It answers one question for tonight: who is actually called.

The story is 4 October 2026. Two posted call sheets exist for that night. The earlier one still names Mara Camilleri as Rosa. The later one supersedes it and posts Lina Borg because Camilleri is off. The public door shows Borg and says why. The earlier sheet stays in the book, marked replaced.

I did not want to overwrite history just to get the correct door. Both sheets stay as documents. The later posted sheet points at the earlier one with supersedes. The door prints the posted sheet for that date that no other posted sheet supersedes.

Demo

https://youtu.be/5juZLfRocXo

The public door is live, in fixture mode, with no login: https://saltwharf.vibefy.net/

Walkthrough, with Lina Borg on the door and the earlier Mara Camilleri sheet marked replaced: https://github.com/mascarock/salt-wharf/blob/main/docs/proof/salt-wharf-walkthrough.mp4

That page is not reading dataset production on project ebwymj6z. With no project id set, the build serves the committed seed. One visitor's backstage moves stay in that visitor's cookie.

Public door: Lina Borg is Rosa

How to judge it:

  1. Open https://saltwharf.vibefy.net/. Lina Borg is Rosa. The slip says a later sheet replaced Mara Camilleri. Camilleri is not called. The book keeps the earlier sheet, marked replaced.
  2. Open /backstage and sign in as elena / callboard. This is a fake contest gate. It does not protect Sanity, GitHub, or any real system.
  3. Cover finder, role Rosa, night 4 October. Borg can cover. Vella fails range. Galea fails a concurrent scene. Camilleri fails the date. Micallef fails because they are already covering. Azzopardi fails a skill.
  4. Advance the 5 October draft. Each move writes a workflowTransition. Posting it does not change 4 October, because supersedes is per date. The moves live in your browser only. Put the book back restores the committed story.
  5. npm run verify asserts the door and the cover checks against the seed. The log is docs/proof/verify.txt.

Backstage sheet stack

git clone https://github.com/mascarock/salt-wharf
cd salt-wharf
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://127.0.0.1:43174. No environment variables are required.

Code

https://github.com/mascarock/salt-wharf

Longer notes than this post: BUILD.md in the repo.

My Build Process

I built this in Cursor as a Next.js app with Sanity behind it. I am not attaching a prompt transcript. The decisions that took the longest are the ones below.

One role, one scene. I kept a role as a character track with one scene, not "Rosa in every scene she plays." Splitting Rosa into Kitchen-Rosa, Storm-Rosa, and Finale-Rosa would make the call sheet honest about the book and ruin the cover story: Borg would already hold three tracks and fail "already covering two." runsConcurrentWith is stored on the scene, in both directions, and the function also walks incoming edges, because a one-way link is easy to forget in Studio.

Coverage is computed. There is no cover document type. A later sheet may annotate an item with note: "cover". That is a house note after a decision, not the source of truth. The check is five predicates: range contains the role's required range, skills cover the role's skills, not cast or called in a concurrent scene, not unavailable that night, not already covering two other roles. Pawlu Galea fails Rosa only because he is cast as the Clerk in the harbour office, which runs with the kitchen. He has the range and the skills. A stored understudy list would have hidden that.

Workflow is documents, not the Workflows product. A workflowTransition records from, to, who, when, and why. Legal pairs are draft → stageManagerReview → posted → struck, plus posted → draft only with a note. Returning a posted sheet to draft takes it out of the posted set, so it stops hiding the sheet it superseded. I did not add a second mechanism for that. I also did not take a dependency on Sanity Workflows, the product. This is workflow-as-data next to the content.

No App SDK app. I read the current quickstart. A Dashboard app needs sanity deploy and Dashboard auth. There is no App SDK deploy on this project, and I did not wrap unused SDK hooks to pretend there is one. What does run after an import is the TypeScript schema, a custom Studio structure (call sheets by status, the company graph, the transition log), and a Callboard tool that resolves the same standing sheet as the public door. In fixture mode, /studio stays parked instead of booting Studio against a missing env.

Skills stayed closed string ids, shared by people and roles, because a skill document would have made the seed harder to import. Vocal range is a pair of pitches, not the word "soprano." Containment is the math.

Sanity Project Details

  • Project ID: ebwymj6z
  • Organization: o1r4ucepz
  • Dataset: production
  • Seed the fixture and the import use: sanity/seed.ndjson
  • Schema: sanity/schema

The live door does not write to that dataset. Live writes need a local write token, and that token is not in the repo.

Agent Session

I did not attach an agent transcript. There isn't one in the repo. The rerunnable proof is the live door, the five steps above, and npm run verify.

Top comments (0)