shebang!

The postcard walkthrough

Building a small third-party app end to end on the public platform -- sign-in, a database with RLS, a scoped key, an image upload, and a published page.

The postcard walkthrough

This walks through building a small app — call it "postcard" — entirely against the public platform, the way an outside developer would: sign up, get a key, provision a database, upload an image, publish a page. Two things trip people up here more than anything else, so they're called out up front rather than left as traps.

Before you start: project_create's color field accepts only a curated palette, not any hex code — pick one of the palette colors the tool lists rather than inventing your own, or the call fails client-side before any request is sent. A raw API caller who bypasses the tool and sends a bad color directly gets a 400 invalid_project from the server instead, naming the same palette (see MCP troubleshooting for the full list). And store_upload_content's contentType is required and is what the tool uses to work backwards: if slug (or path, when given) has no file extension, the tool infers one from contentType (image/png becomes .png, and so on) — only when neither supplies a usable extension does it fail, and it fails before any network call.

1. Sign up and sign in

Create an account at dash.shebang.pro/signup — password sign-up requires confirming a 6-digit code sent by email, not a confirmation link. Sign in once confirmed.

2. Install the CLI and log in

npx -y shebang-mcp login

This runs the device-authorization flow — see device login — and writes a master key to your local credentials once you approve it in the browser.

3. Create a project

project_create({ name: "postcard", tag: "postcard", color: "#3b82f6", slug: "postcard" })

Use one of the curated palette colors (the friction point above) — the tool rejects anything else client-side before the call is even made. A raw API caller bypassing the tool's validation gets a 400 invalid_project back instead, with a message naming the palette.

4. Mint a scoped app key

key_create_app({ name: "postcard app", apps: ["page", "serve", "link", "base"], project: "postcard" })

The plaintext key (shb_…) is returned exactly once — this is what your running app authenticates with, never the master key from step 2.

5. Provision a database and add RLS

base_create_database({ slug: "postcard", project: "postcard" })

Then, over base_run_sql, one statement per call (see limits):

create table postcards (
  id uuid primary key default gen_random_uuid(),
  owner uuid not null default auth.uid(),
  title text not null,
  message text not null,
  image_url text,
  created_at timestamptz not null default now()
);
alter table postcards enable row level security;
create policy "anyone can read" on postcards for select using (true);
create policy "owner can insert" on postcards for insert with check (owner = auth.uid());
create policy "owner can update" on postcards for update using (owner = auth.uid());
create policy "owner can delete" on postcards for delete using (owner = auth.uid());

Then base_reload_schema so the new table shows up over the Data API immediately, without waiting for PostgREST to notice on its own.

6. Upload the postcard's image

store_upload_content({ slug: "postcard-image", contentType: "image/png", contentBase64: "<base64>", project: "postcard" })

slug and contentType are required, and exactly one of contentBase64 or text is required too. path (the second friction point above) is optional and only ever used as a filename hint — omit it, as above, and the tool falls back to slug for the name. Either way, if that name has no file extension, the tool infers one from contentType (image/png becomes .png); only when neither supplies a usable extension does the call fail client-side, before any network call, rather than surfacing the platform's own 415.

7. Publish a page and share it

sherpage_publish({ title: "About postcard", files: [...], access: "public" })

Every publish and upload gets an automatic short link — link_list (scoped to project: "postcard", since a key's calls default to its own home project — see projects and scoping) finds it. sherpage_set_access/link_set_access change the access level afterward if you didn't set access: "public" at creation time.

What to expect from RLS

Sign in as a second user and try to update or delete another user's row directly through supabase-js — it silently affects zero rows rather than raising an error. That's the expected shape of a Postgres row-level-security block on UPDATE/DELETE: the row is invisible to the statement, so nothing explicit ever complains. A follow-up read confirms the original row is unchanged.

Next

Sign in with sherlock — the same sign-in flow, from the app side, for users signing into your app rather than the dashboard.