Security and privacy
How publishers’ code is isolated, and what the platform holds.
On this page
- What is live today
- Your code
- Its own container
- No secrets, bindings or network
- Static demos
- A separate domain
- Pages
- Secrets never leave your machine
- Mock data only
- What the platform holds
- About you and your workspace
- About your viewers
- About community members
- Files are served by their row
- Who can see it
- Workspace isolation
- Viewer links
- Signing in
- The app
- pitch
- Claude acts as you
- Deleting a project
Roomi runs code written by other people, and holds records that are not mock data even though every demo is: who was shown a pitch, what they did and what they wrote back, and plans for businesses that have not launched. This page says how your code is kept apart from everything else, what the platform holds about you and your viewers, and who can see it.
What is live today
Roomi is in early access: the app at app.getroomi.com, the API
at api.getroomi.com, the rooms and demos on getroomi.app, and
the connector at mcp.getroomi.com. Roomi keeps a written
security gate for what it runs; its lines are:
- authentication in front of every route, checked by requesting each one signed out;
- one workspace refused another's projects, links, analytics, feedback and files by id, checked by attacking it;
- no address answering that nobody chose, and no preview addresses;
- viewer links unguessable, expiring, revocable, and checked on every request;
- publishers' containers out of reach of every platform binding and secret, and demos on their own domain;
- secrets in the host's secret store, none in a committed file;
- storage private, confirmed from an unauthenticated client;
- a dependency audit clean of critical and high findings;
- passkeys confirmed in production, from a real device;
- a written threat model.
The rest of this page describes the system as it is built.
Your code
Its own container
A container demo runs only in a container of its own,
never inside the platform. The platform does not run your image: it runs one
runner image of its own, Node 24 and a small supervisor, and hands it your
build, the /app folder your Dockerfile produced. A container only ever runs
one build, refuses a second, and stops when the build does. Your code runs as
the node user, not root, and owns /app and nothing else.
Each private link gets its own container, so one viewer's clicks never reach another's copy. The platform never unpacks a container bundle itself: it checks that the upload is a gzipped archive and stores it, and only the runner, inside the container, unpacks it.
No secrets, bindings or network
A demo's container is started with two port numbers and nothing else:
- no platform binding: no database, no storage, no route back into the platform;
- no secret of the platform's in its environment, only the
ENVyour own image declared; - no internet: the bundle is streamed in over a control port, so the demo never needs to fetch anything, and cannot.
The viewer's own pass into the demo is taken out of every request before it reaches your server, so your code never sees it.
Static demos
A static demo is files: HTML, CSS, JavaScript and images. The platform unpacks the archive into private storage and serves the files as they are. None of it runs on the platform; it runs in the viewer's browser, on the demo's own origin.
A separate domain
Rooms and demos live on getroomi.app, a different registrable domain
from getroomi.com, where the app, the API and sign-in are. Every
project gets two origins there:
<project>.getroomi.appis the room: the stage, the tour, the notes, comments and next steps.<project>--demo.getroomi.appis your demo, framed by the room.
Two reasons. A demo's script can set cookies for its parent domain, so on a
shared domain one could plant a session where people sign in. And one
phishing page deployed by a stranger would otherwise get the product's own
domain flagged. getroomi.app is to go on the Public Suffix List, so that
browsers treat every project as a separate site; until then, the room refuses
any write that does not come from the room's own page.
The room's pages run only the room's own script and styles, talk only to the
room, frame only their own demo, and are framed by nobody. The demo may be
framed by its own room and by its own pages (a showcase framing its other apps),
and nothing else; a frame-ancestors or
X-Frame-Options of your own is replaced by that rule, and the rest of your
content security policy is kept.
Pages
A page in the room's Read more rail is rendered sandboxed: an HTML page runs no script, loads nothing from the network, and has no origin of its own. See Pages.
Secrets never leave your machine
pitch deploy scans for secrets with gitleaks
before anything is uploaded, and again on what it built:
- The project folder, leaving out installed dependencies (
node_modules), git's own store, and the folders build tools generate (.next,.turboand the like, where Next writes keys of its own). Whatever of them would ship is scanned in the next step. - The build: a static demo's output folder, or a container demo's bundle
together with the
ENVit will start with, since a secret baked into an image's environment would ship in its manifest.
A finding stops the deploy, and nothing is uploaded. The report names the rule
and the file and line, with the value redacted, and asks you to remove the
secret and rotate it, since it has been on disk in plain text. Your own
.gitleaksignore is not read, so a finding cannot be waved through from inside
the project. A scanner that could not run is a failed scan, not a clean one:
the deploy stops there too.
While it rehearses, pitch talks only to the copy of your build it started on
this machine's loopback address. Nothing leaves the machine until pitch deploy --commit, which uploads exactly what was rehearsed.
Mock data only
Every viewer gets a copy of your demo, so nothing in it may be real: no customer data, no production keys, no connection to a real system. A demo's container has no internet, so it could not reach one anyway. The room tells every viewer so: "This demo runs on mock data. Nothing you do here reaches a real system."
What the platform holds
About you and your workspace
- Your account: your name, email and avatar as GitHub or Google gave them. Sign-in sessions record the address and browser they were made from, as the sign-in library does. For a passkey, only its public half is kept.
- Your workspace's members, projects, builds, pages, brand, community card and cover, next-step settings, and your notes and follow-ups on viewers.
- Your builds' archives and bundles, in private storage.
About your viewers
- The name you gave each private link, and the email if you gave one.
- The people who opened each link: the person it was made for, and anyone it was forwarded to who said who they are, with the name and email they typed. No email is ever sent, so none is verified.
- Their visits: the time spent on each app and page, and the moments of each visit, with a device class (phone, tablet or desktop). No IP address, browser version or location. See Analytics and the pipeline.
- What they wrote and chose: comments, where they were pinned, the replies, next-step answers and reactions.
- At a public address: an anonymous visitor id, stored hashed and used only to count people.
About community members
Someone who signs in to comment or react at a listed project's public address is known to the room, and to you, by their public face alone: the name their sign-in gave them, their handle if they chose one, and their avatar. Their email never reaches the room or the publisher.
Files are served by their row
Logos, covers, pages and build files are stored privately. They are read by the platform from the database row they belong to, in your workspace, and streamed; no storage address or key ever reaches a browser.
Who can see it
- Your workspace's members see everything the workspace holds. See Teams and invites.
- A viewer sees the room their link opens, and your replies to their own comments. Never anyone else's.
- Anyone sees only what you chose to make public and that passed Roomi's checks: a listed project's card, cover and public address, and your public page's name, avatar, handle, bio and listings. Never an email.
- Roomi sees what running the service needs: workspaces, their members' emails, projects, plans, listings and the checks run on them, and reports. Every action it takes on your workspace carries a reason and is recorded.
Workspace isolation
Every request the API answers is scoped to the workspace you are working in, in one data-access layer that every query goes through. Asking for another workspace's project, link, visit, comment, note or file by its id is answered "not found", the same as an id that does not exist, so a refusal says nothing about what exists. The rooms get a narrower door still: find a project by its address, check a link, and record what that link's viewer does.
Viewer links
- A link's token is 128 random bits. The room finds a link by a hash of it. Beside the hash the token is kept encrypted (AES-256-GCM) with a key that lives only in the platform's secret store, never in its database, so a member can be shown the address again. Someone who read the database alone could open no room. Each time an address is shown again it is recorded in the project's history, and a connection that may only read is never shown one.
- When a viewer opens it, the room swaps the token for a session cookie of its own, for the room's host alone, and takes the token out of the address bar. The room sends no referrer.
- Every request is checked against the link again, not only the first. A revoked or expired link, a paused project, or a link that is not on the plan any more gets the same plain "Not found" as a wrong token.
- The pass that joins a phone to the same copy ("Open on your phone") lasts ten minutes. The one-time passes the app makes for "Preview as viewer" and for a community sign-in work once, within two minutes, on their own project's address only.
Every project starts unlisted: without a link, a preview pass, or an approved community card, its room answers nobody.
Signing in
The app
Sign in with GitHub or Google; the same button signs you up. Once signed in, you can add a passkey in Settings and sign in with it after that. Sign-in by emailed link is offered only where the platform can send email, and today it cannot.
pitch
pitch login prints a code and a page to open; you confirm the code in the
app, where you are signed in, and only pitch can ask for such a code. The
token goes in the macOS Keychain, never in a file; pitch logout ends the
session on the server too. For a script, PP_TOKEN is read and never stored.
See Install the CLI.
Claude acts as you
The connector signs in by OAuth through Roomi. When Claude connects, the app shows what it may do, and you allow or deny it:
| Permission | Lets Claude |
|---|---|
pitch:read |
See your projects, their builds, viewer links, visits and feedback |
pitch:write |
Deploy demos, create and revoke viewer links, and add pages to rooms |
offline_access |
Stay connected until you disconnect it |
A token is issued for the connector alone, for the workspace you were in when
you allowed it, and every call is checked again: a connection allowed to read
cannot change anything, and one whose person has left the workspace is
refused. Claude never holds a cloud credential or your pitch token. Each tool
that changes something says so, and Claude asks you before using it. The
consent page warns you when the app asking is not Claude. See Deploying from
Claude.
Deleting a project
Deleting a project, in its Settings tab, takes it out of your workspace and stops its links at once. Its address is held for 30 days so a link already sent cannot open someone else's project. The project is marked deleted rather than erased: its records stay in the database.