Backstory
A whiteboard and one sentence
- Artifacts are the pages Strive’s agent writes in chat: reports, guides, plans. Our CTO sketched two flows on a whiteboard, signed in and shared, and the brief was one sentence: let anyone with the link open one, without signing in, as an add-on to the artifact and file work that already shipped.
- I own the frontend, and backend isn’t my home ground. So I paired with Claude Code on the investigation, the design doc and the server side, and kept the brief, the product and scope calls, the testing and the redesign. Design first, reviewed, then build.
The find
The frame never had a session
- Before drawing any boxes, I read what had already merged. Artifacts render in a sandboxed frame, so the browser never sends it the login cookie anyway. Every request it makes carries a signed token in its path.
- So a public viewer is just someone without a session, and the frame never had one either. The link only needs another way to hand out that token. The page route, the image route and the sandbox stay as they are.
- Viewer · their tab anyone with the link, signed out
- Frame · sandboxed the page the agent wrote
- Strive · server the routes that already ship
- Viewer → Strive 1/share/‹slug› no sign-in, no cookie
- Strive 2Slug → a live link then mints the token the frame already uses
- Strive → Viewer 3title · org name · frame URL
- Frame → Strive 4page + images, token in the path no cookie, as always
- Strive 5One new check is the link still live?
Design call · the link
A public link is a password
- My first question: could the Clerk session token be the share credential, or did we need our own JWT? Neither. A session token would hand out the sharer’s whole account, and a JWT can’t be turned off without a table anyway.
- So the link is a random id in a row, the way ChatGPT and Claude do it. When our CTO asked how the string is made, I compared it with UUIDs, nanoid and short codes. A link anyone can forward is a password, so its length is a security setting, not a style choice.
Session token or a JWT
- A session token hands out the sharer’s whole account
- A JWT can’t be turned off on its own
- A JWT runs 200+ characters anyone can decode
vs
A random id in a rowpicked
- Opens one page, read only
- Stops working on its next request
- 22 characters, 128 random bits
Design call · images
Scope it to the page, not the workspace
- The sketch had shared links check that an image belongs to the workspace. It looks like a fair simplification. But file ids count up, so one public link could have walked from
asset/1toasset/N: every image the workspace ever uploaded. - So images stay checked against the page they belong to, the check that already shipped. I put each correction in the design doc next to the code it came from, so it read as a fact, not an opinion.
In the sketch · the workspace
- One link, every upload in the workspace
- Ids anyone can count up
vs
Shipped · this pagekept
- Only the images on this page
- A guessed id gets a 404
Design call · the table
One table for every shareable thing
- The first draft had a share table just for artifacts. Reviewing it, I asked for one generic table instead, the way our relations table already works, because reports and chats would want sharing next.
- Each kind plugs in a small resolver: does it exist, what does a version mean for it, and what does a stranger see. Reports and chats can join without a migration or new routes. Generic one step ahead, and no further.
share link
- entity type
- entity id
- pinned version
- slug
- turned off?
- Artifact now resolver
- Report next resolver
- Chat later resolver
Does it exist? What does a version mean for it? What does a stranger see?
Before · after
Access is a state, not two buttons
- The first version worked. Then I used it, and changed the model rather than polishing it. Claude and ChatGPT keep sharing and exporting under one control, so Share and Export became two tabs of one, with room for people and roles later.
- “Create public link” and “Stop sharing” became General access: members only, or anyone with the link. Picking one turns the link on, and picking the other turns it off. The bare share glyph became a small labelled Share button.
Before
After
The other side
What a stranger sees
- A shared page is bare: the title, “Shared from” and the workspace’s name, the page itself and a small “Made with” mark. No sign-in, no chat, no app around it. The name builds trust and costs nothing.
- A dead link is a first impression too, for someone outside the company. So the plain error became a branded page that says why in one line and offers a way in.
- Title, and who shared it. “Shared from” and the workspace’s name.
- Made with. A small mark, and the partner’s own on a white-label domain.
- The page, untouched. The same stored page, images included, in the same sandbox.
Before
After
The details
Small calls, said out loud
- Nobody sees most of these. Each one is still a decision, so each has a reason written down.
-
Pinned to what you shared
A link shows the version it was shared at. Later edits from the agent stay private until someone moves the link to the new version.
-
A dropdown I didn’t build
Claude uses a dropdown for General access. Inside our popover, a dropdown opens a layer of its own and closes the popover on the first click. Two radio rows do the same job.
-
Arrow keys that can’t publish
A review suggested arrow keys between the two choices. Radios pick as focus moves, so one stray key press could make a page public. I said no.
-
Guests still get Export
A guest can’t share, but can still download. The control shows what each person can do, and hides when there’s nothing.
-
Safe on a partner’s domain
The dead-link page and the “Made with” mark take the partner’s own name and logo, so a white-label domain never says Strive.
-
Every dead link looks the same
Turned off, deleted or never there: the same page, so the address tells a stranger nothing.
Smudges
Two 404s and a leaky address
- My first local test returned a 404, and the next one broke artifacts even for signed-in people. Neither was in the code: a local proxy forwarded only a fixed list of paths, and the local render secret was empty. I added the missing paths first. A review then asked why the proxy needed a list at all, so it became one catch-all, and the next new route can’t quietly 404.
- My own code review caught a real one. Product analytics recorded every page address, so the share link went along with it, and anyone with analytics access could have opened shared pages. The link is now masked before any event leaves the browser.
What dried
What I’d keep
-
MissFixed the proxy by adding the two missing paths.
LessonFix the mechanism, not the symptom. A list of paths will always miss the next one.
-
MissBuilt the first popover around “Create public link” and “Stop sharing”.
LessonMake access a state you set, not two actions you hunt for.
-
LessonRead the code before drawing boxes. Half the feature had already shipped.
-
LessonSay no on purpose. Every review finding I skipped has a one-line reason, usually that it would change something people rely on.
The best feature was the one already half-built.