wirldocs

Sharing and who can open what

A new app's starting visibility depends on the organisation it is deployed to. It starts visible to everyone in the org when the org has a verified company email domain. In any other org, including a personal one or one made with wirl orgs create, it starts private: only its owner, the organisation's owners and admins, and the people it is shared with can open it. Sharing by email only ever adds one named person. The visibility level itself is changed with wirl visibility private or wirl visibility org (the wirl_visibility tool for an agent), or on the app's Access page; making an app public is only ever done there, by a person.

Who can open it#

Three levels of visibility:

  • Private — only its owner, the organisation's owners and admins, and the people it is shared with by email.
  • Everyone in the org — anyone signed in at the owning organisation.
  • Anyone with the link — anyone at all, signed in or not.

wirl visibility with no level prints which of the three an app has. wirl visibility private and wirl visibility org change it, and so does an agent through wirl_visibility; both need a full-scope sign-in, as sharing does. Public is different on purpose: it opens the app to people nobody chose, so it is a decision a person takes with their own click, on the app's Access page in the dashboard. Neither the CLI nor any tool an agent can call will do it, and there is no flag that makes them: asked to, they answer with that page's address.

wirl deploy and wirl_deploy end by saying which of the three the app has (visibility in --json): the level the edge was given when the deploy finished, read as it wrote the app's address, so a change someone made while the deploy ran is the one it reports.

A form post, or any other request but a GET, HEAD or OPTIONS, that another app's page or another site sends to an app arrives as if the person were signed out: an app that is not public never sees it, since wirl answers it with sign-in, and a public app gets it with x-wirl-user-id and the other identity headers empty. A public path is served signed out whoever asks, so there such a request reaches an app that is not public too. The app's own pages, links into the app and scripts outside a browser are unaffected. A page that sets its own no-referrer referrer policy makes its own form posts arrive signed out too, and in some browsers its beacons and its fetch calls in no-cors or same-origin mode, so leave that policy to wirl.

Three things are left to the app. Answer a post with a redirect, never a page: on a public app, Chromium sends another page's post again as the app's own, with the person signed in, when the page it was answered with reloads itself from script, and after a redirect a reload asks for the page and sends nothing. A service worker should leave page loads, and every request but a GET, to the network. Another page reaches an app's service worker only through a page load, and a worker that passes one on sends it as the app's own, with the person signed in: some browsers do so for fetch(event.request), and every browser for a request the worker builds or changes. A worker that answers page loads also keeps wirl's sign-in from finishing. And a cookie the app sets for itself arrives with another app's post whatever its SameSite, because every app is the same site, so an app that keeps its own session in a cookie must check that a post's Origin is its own.

A cookie the app sets in a Set-Cookie header is kept to the host that set it. wirl removes any Domain attribute from the app's Set-Cookie headers and passes every other attribute on as the app wrote it, so the browser sends the cookie back to that host alone: not to another app on wirl.run, and not from an app to one of its previews or back. A page's own script is not held to this. Until wirl.run is on the Public Suffix List, another app's page script can still set a cookie for all of wirl.run (document.cookie with domain=wirl.run), and this app will receive it, so never treat a cookie's presence as proof the app issued it: check a session cookie against what the app stored, for example by signing it or looking it up. A cookie set for all of wirl.run before wirl began removing the Domain stays until it expires unless page script clears it, because wirl removes the Domain from a Set-Cookie that deletes a cookie too. Page script can clear one that is not HttpOnly (document.cookie = 'name=; Max-Age=0; domain=wirl.run; path=/'), but in Chromium and Firefox an HttpOnly one stays until it expires, so give a session cookie a new name rather than reuse one that was set for all of wirl.run.

For the same reason wirl takes the "cookies" and "*" directives out of an app's Clear-Site-Data header, and drops the header when nothing else is left, so the header clears no cookies: an app clears its own by expiring them with Set-Cookie. Chromium clears cookies for the whole registrable domain, which is all of wirl.run until it is on the Public Suffix List, so one app's header would clear every app's cookies and sign the person out of each. "cache", "storage" and "executionContexts" clear only the app's own origin and arrive as the app sent them; an app that sent "*" and wants those cleared names them.

Roles on an app#

Sharing an app names someone as an editor or a viewer:

wirl share dana@company.com

That person can now open the app, as a viewer; add --role editor when they should be able to deploy it too. An agent does the same with the wirl_share tool, and a person with the share form on the app's Access page. Sharing again with someone who already has access, or whose invitation is waiting for their first sign-in, leaves the role they have unless you name one; naming a role sets it, an editor lowered to viewer included, and replaces any invitation still waiting for them. When a share keeps a role or changes one, the answer says so, and from what when it changed one, and the organisation's Activity page shows the role and the one it was before. A role that is neither viewer nor editor is refused with a sentence naming them, and nothing is shared. The command line and the local MCP server do this from 0.2.2 (npx wirl fetches the newest), as do the API, the dashboard and the hosted /mcp; an older command line names viewer itself when you name no role, which lowers an editor. The roles:

  • Owner — can view it, deploy it, and everything administrative on top: change who can open it, share and unshare, transfer it, move it to another org, delete it.
  • Editor — can view it and deploy it. Cannot change who can open it, share it, transfer it, move it, or delete it. The code an editor deploys is handed the address and name of everyone signed in who opens the app, in the identity headers, so anyone you make an editor can learn who uses it.
  • Viewer — can view it. Nothing else.

Taking a share back names the person too:

wirl unshare dana@company.com

An agent does the same with the wirl_unshare tool, and a person with the app's Access page. It takes the whole share away, one person at a time — to change a role instead, share again with a role — and it withdraws an invitation that is still waiting for the address. On a deleted app, while it can still be restored, the same people may take a share back, and a restore brings none of it back. On a live app, whoever may share may unshare: the app's owner while a member of its organisation, or an organisation owner or admin, always with a full-scope sign-in. Finding nothing to take back is said as nothing, and the organisation's Activity page shows what was taken. A script reads what was found from wirl unshare --json: shareRevoked and invitationWithdrawn are each true only for what was taken, and appDeleted says the app was deleted when this was asked. revoked is always true, kept for older callers: it says only that the request was carried out, so read the other two.

Org roles#

An organisation has owners, admins and members. An org's owners and admins each count as an owner of every app in that org, whether or not anyone shared it with them by name — so putting an app in an org puts it within reach of whoever administers that org. A plain member gets no standing role on an app unless it is shared with them, except that an org-visible app is readable by every member; reading it grants no right to change or deploy it.

An organisation made for a company's email domain is made by the first person who signs in with a verified account on that domain, as its owner, and everyone who signs in from the domain after them joins it at that sign-in: as a member, or as an admin if an invitation waiting for them says so. Someone removed from it, or who leaves it, does not rejoin by signing in: they stay out until an owner or admin adds them back, and their own sign-in lands them in an organisation of their own.

Which organisation a sign-in lands in#

wirl puts people together by the company domain Google vouches for, which only a Google Workspace (or Cloud Identity) account carries. Everyone who signs in from a Workspace domain such as acme.com lands in that domain's organisation, as above. A Gmail address, or any Google account that is not on Google Workspace, carries no company domain, so its first sign-in makes it an organisation of its own, with the person as its owner and a short address-style name taken from the part of the address before the @. Two people on gmail.com are never put together by it.

The same happens at a company whose mail is not on Google Workspace, Microsoft 365 say, when its people sign in with Google accounts made for their work addresses: Google does not vouch for the domain, so each lands in an organisation of their own. To work together, one of them makes an organisation with wirl orgs create <slug>, which makes them its owner, and adds the others with wirl members add <email>, or shares each app by email. Both need a full-scope sign-in. Someone added who has not signed in yet joins at their first sign-in, and everyone added keeps their own organisation too. A new app in an organisation made that way, or in a personal one, starts private, as the top of this page says, so the people added see what is shared with them or set to everyone in the org.

Removing someone from an organisation, or leaving it, ends the membership and what it gave: the apps there open to everyone in it, its connections, and an owner's or admin's rights over every app there. It revokes no share: an app shared with them by name stays shared, and still opens for them, and an editor's share still deploys it, with the connections the app is granted, until the share is revoked, as on the app's Access page. For an owner or admin, the removal's confirmation page and wirl members remove list those apps, and the deleted apps there that are shared with them by name too, while those can still be restored: deleting an app revokes none of its shares, and a restore brings each back. Each deleted one is marked "deleted; a restore brings this share back", with the day its data is kept until, and linked to a page that revokes its share, before the removal or after it.

An app's owner holds the owner's rights only while they are a member of the app's organisation. Removed from it, they lose them at once: a sign-in or token they kept no longer lets them administer the app, deploy it or open it as its owner, and a visit already open in their browser keeps the owner's role only until its next renewal, as any access taken away does (Limits). A preview's visit link they printed before they were removed is the exception: it still lets whoever opens it into that preview as its owner, for up to 29 minutes after it was printed. The app stays with the organisation's owners and admins, who can transfer it to someone else. An app shared with them by email stays shared: a share is not ownership.

Sharing before someone has signed in#

Sharing works even for a colleague who has never used wirl: the invitation waits for their first sign-in and is theirs the moment they arrive.

Transfer and move#

wirl transfer dana@company.com

Hands the app to a new owner. The previous owner keeps editor access, as a share, if they are a member of its organisation when it is handed over; an owner the organisation has removed gets none.

wirl move --to acme

Moves the app to another organisation you belong to. Its owner holds the owner's rights there only if they belong to that organisation too. When they do not, the move form on the app's Settings page says so before you move it, and wirl move says so after.

Its connection grants do not go with it: a connection belongs to the organisation that made it, so the move drops every grant the app holds, requested and revoked ones included, and wirl move names the connections it dropped. The app's next deploy asks for the connections its wirl.json declares in the new organisation, as any deploy does. A grant never opens a connection for an app outside the connection's organisation, so a deploy, a preview or a connection added on the app's page while the app is being moved leaves it nothing in the organisation it left: a grant written before the move is dropped by it, and one asked for after it is refused, saying the app was moved.

Where this lives in the dashboard#

An app's Access page holds its visibility setting, the people it is shared with and their roles, and the share form. Its Settings page holds the transfer form and, once you belong to more than one organisation, the move form. Both are visible only to someone who administers the app. Settings also holds the switch for the wirl badge: the badge on the app's pages reads "Built with wirl" and names no one, the owner included, and someone who administers the app can turn it off there. Settings also holds Delete, which asks for the app's name typed back; a deleted app can be restored for 30 days from the Recently deleted list on the organisation's tools page. An organisation's Members page is its roster: who belongs, their role, and a form to change either.