Problem RestatementProblem
Design a product where people share AI prompts (asked at Anthropic twice). A user writes a prompt (maybe with variables and settings), then shares it privately, with specific people or their team, via a link, or publicly. Others can view, run (with their own account and credits), or remix (copy and edit, keeping attribution). The owner can revoke access. Shared versions must be stable: if the owner edits later, people who were given version 3 still see version 3 unless they choose to update.
RequirementsRequirements
- Create prompts with versions (immutable once published).
- Share settings per prompt: private, specific users or groups (view or edit), anyone with the link, public.
- Run a shared prompt safely, using the runner's own quota. Never expose the owner's API keys or hidden data.
- Remix: fork into your own copy with a "remixed from" link.
- Revoke: links and grants stop working immediately.
- Discover public prompts: search, tags, popularity.
Data ModelData model
CREATE TABLE prompts (prompt_id UUID PRIMARY KEY, owner_id UUID, title TEXT, visibility TEXT, -- private, link, public
latest_version INT, remixed_from_version_id UUID, created_at TIMESTAMP);
CREATE TABLE prompt_versions (version_id UUID PRIMARY KEY, prompt_id UUID, version INT,
body TEXT, variables JSONB, model TEXT, params JSONB, published_at TIMESTAMP);
CREATE TABLE prompt_grants (prompt_id UUID, principal_type TEXT, principal_id UUID, role TEXT, -- viewer, editor
PRIMARY KEY (prompt_id, principal_type, principal_id));
CREATE TABLE share_links (token_hash TEXT PRIMARY KEY, prompt_id UUID, version_id UUID NULL, -- NULL = latest
role TEXT, expires_at TIMESTAMP, revoked BOOLEAN);
CREATE TABLE prompt_stats (prompt_id UUID PRIMARY KEY, views BIGINT, runs BIGINT, remixes BIGINT);- Versions are immutable, which gives stable sharing and exact reproducibility.
- A share link can pin a version, or follow the latest.
- Link tokens are random, and only their hash is stored (like passwords).
ArchitectureArchitecture
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
U["Users"] --> API["Prompt API"]
API --> AUTHZ["Permission check"]
AUTHZ --> DB[("Prompts, versions, grants, links")]
API --> RUN["Run Service - uses runner's own quota"]
RUN --> MG["Model Gateway"]
API --> K[("Events: published, shared, remixed")]
K --> SRCH["Search index - public prompts"]
K --> MOD["Moderation"]
K --> STATS["Stats counters"]
DISC["Discover page"] --> SRCHKey FlowsFlows
- Open a shared prompt: resolve by grant (user or team) or link token → check it isn't revoked or expired → return the allowed version.
- Run: the run service loads the version, fills in the runner's variables, and calls the model gateway using the runner's account and limits. The owner's secrets are never part of a prompt; if a prompt needs tools or keys, the runner must connect their own.
- Remix: copy the version body into a new prompt owned by the remixer, and set
remixed_from_version_id. The attribution chain is a linked list you can follow back to the original. - Revoke: delete the grant or mark the link revoked. Permission checks happen on every request (with only very short caching), so access ends quickly. Already-made remixes stay with their owners (they're copies), which is a policy to state clearly.
- Publish publicly: moderation checks first (harmful content, embedded personal data or secrets), then indexing for search.
Deep Dive — Four ways to share one promptDeep dive
A prompt can be private, shared with named people, shared with a team, shared by link, or fully public. Modelling that badly is how permission bugs get shipped.
A visibility column
visibility is an enum: private, team, public. The check reads the column.
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
P["Prompt - visibility: team"] --> C["Check: is the viewer in the owner's team?"]
C --> NO1["Cannot share with one named colleague"]
C --> NO2["Cannot share with a second team"]
C --> NO3["Link sharing needs its own column, then its own code path"]
NO3 --> DRIFT["Two sources of truth about who can see this"]The enum holds one relationship, and sharing is inherently many. The first "can you also share it with Priya?" forces either an abuse of the enum or a second mechanism beside it — and once there are two mechanisms, they disagree.
Keep the enum, add an ACL table
Add prompt_acl(prompt_id, user_id, role) for named people, and keep the enum for private/team/public.
This covers the cases, and it has two sources of truth for the same question. Every check has to consult both and combine them, so the precedence rules ("public but explicitly removed?") live in application code and get re-implemented slightly differently in each endpoint. Link sharing still does not fit either structure.
Everything is a grant
One table. A grant says who may do what, where "who" is a principal of some kind:
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
G[("grants: prompt_id, principal, capability, expires_at")] --> P1["principal: user:42 - capability: edit"]
G --> P2["principal: team:eng - capability: view"]
G --> P3["principal: link:tok_9f3 - capability: run, expires in 7 days"]
G --> P4["principal: everyone - capability: view"]
CHK["check(viewer, capability, prompt)"] --> EXP["Expand viewer: themselves, their teams, any link token presented"]
EXP --> MATCH["Match against grants - highest capability wins"]
MATCH --> DEC["Allow / deny"]Public is a grant to everyone. A share link is a grant to a token, which is what makes link sharing fall out of the same model instead of needing its own: the token is just another principal, and it can carry an expiry and a capability of its own. Revoking a link is deleting one row.
Three consequences worth stating:
- One check, one code path. Every endpoint asks the same question, so there is no endpoint that forgot a case.
- Capabilities, not roles.
view,runandeditare separate, because "you may run this prompt but not read its text" is a real product requirement here and an enum cannot express it. - Running costs money. A
rungrant means the viewer's credits are spent, so the check is also a billing boundary — and a public prompt must never grantrunagainst the owner's account.
Visibility in the UI is then derived from the grants rather than stored beside them, so the badge a user sees and the answer the server gives can never disagree.
Safety and Abuse
- Prompt injection from shared prompts: a shared prompt could try to trick tools into leaking the runner's data. Runs of shared prompts get restricted tool permissions by default, and the UI shows what the prompt will access.
- Secret scanning: detect API keys or passwords pasted into prompt bodies before sharing, and warn or block.
- Spam in public discovery: rate limits, reputation, reports, moderation.
Trade-offs & AlternativesTrade-offs
| Decision | Choice | Why | Alternative |
|---|---|---|---|
| Versions | Immutable, pinned by links | Stable sharing, reproducible runs | Edit in place: shared prompts change unexpectedly |
| Permissions | Grants + hashed link tokens, checked each request | Fast revocation | Long-lived cached ACLs: slow revocation |
| Running | Runner's own quota and credentials | No cost or secret leakage to the owner | Owner pays: abuse risk |
| Remix | Copy + attribution link | Independent edits, credit kept | Shared editable doc: conflicts |
Wrap-UpWrap-up
Store prompts with immutable versions, and share them through explicit grants (users or teams, viewer or editor) and hashed, revocable, optionally version-pinned link tokens, checking permissions on every request. Run shared prompts with the runner's own quota and restricted tool access, remix by copying with an attribution chain, and moderate plus secret-scan anything made public before indexing it for discovery.