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.