Vibes DIY
Vibes DIY / Docs
creator docs · concepts · access

Run a public app for your team

There are two different questions about who can see what, and it helps to keep them apart.

The first is what the app means by access: this document belongs to that channel, people with this role can write to it, my followers can read my picks. That lives in the app's access.js, it ships with the code, and everyone who copies the app gets it verbatim. The Access API covers it.

The second is the posture of your particular copy: who is allowed near this deployment at all. That is set by whoever owns the copy, in settings, without touching a line of source. It is the layer this page is about.

Keeping them separate is what lets you do this:

Find an app that does what your team needs. Clone it. The access logic comes along unchanged. Point its databases at your own people, and run it for your workgroup — with the platform doing the enforcement, not the app.

No fork of the code. No custom build. No editing someone else's access.js and inheriting the job of maintaining it.

The layers, in order

Every read and every write passes the deployment gate first, then the app's own logic:

request
  │
  ├─ 1. posture           ← your deployment, in one word: open, gated, private.
  │                          Who may be near this copy at all.
  │
  ├─ 2. database pin      ← your deployment, per database. Optional.
  │                          Can only ever narrow what the posture allows.
  │
  └─ 3. access.js         ← the app. Ships with the source.
                             Channels, roles, followers, per-object sharing.
  │
  └─ the document

The order matters, and so does the direction. Together the posture and its pins are the envelope, and the envelope is a ceiling: the app's access function can restrict further, but it can never hand out access the envelope has already refused. If you pin a database to your editors, nothing the app does — no channel, no role, no audience — lets anyone else read it.

Inner layers only narrow, and that is exact rather than a rule of thumb. A pin is intersected with the posture, so it can only ever subtract; there is no pin that widens a posture and no access function that widens a pin.

The three postures

A deployment's posture is one word, and it is the whole of the outer layer. Each one names who may read and who may write, before your app's rules get a say:

Posture Who can read Who can write In plain words
open anyone signed-in Reachable by anyone; documents governed by your app's rules.
gated anyone members Anyone can open it; the people you approve can write.
private members members Only approved members can open this vibe.

Two of those rows are decisions worth saying out loud. gated is readable by anyone on purpose — looking first and asking to join afterwards is the point of it. And private lets any granted role write, not editors only; who may write what is still your access function's business.

The two new names in that table are the outermost groups:

  • anyone — every visitor, signed in or not.
  • signed-in — anyone with a Vibes account, whether or not you have ever heard of them.

signed-in is what makes an open app an open app: a stranger who signs in may write, without you granting them anything and without becoming a member of your workgroup. If that is more than you want, gated is the same app with writing held back for people you approve.

Reading open correctly

open is the posture most published apps have, and it is worth being precise about what it claims, because the scary reading is the wrong one. The sharing panel puts it this way, and so does this page:

Reachable by anyone; documents governed by your app's rules.

It does not mean "all your data is public". An app can be open and still keep most of its documents private — a suggestion box that anyone may post into while only you can read the replies is an honestly open app whose access function routes the replies to a channel nobody else is in. The posture says how far the room extends; your access.js still says what each document is.

A clone starts closed

When you clone an app, its public posture does not come with it. The copy is created with public access off and access-requests on, even if the app you cloned from was open to the world. Nobody arrives just by having the link; people have to be let in.

So the starting point for a team copy is already private. What is left is deciding who your team is, and how far into the app each of them goes.

Your team is the grant list

Pins do not carry their own list of people. They talk in terms of four groups, and those groups are simply projections of the access you have already granted:

Group Who that is
editors people you granted editor
submitters people you granted submitter
readers people you granted editor or viewer
members anyone you granted any role at all — editor, viewer, or submitter

Read those rows literally: a role is an explicit grant and nothing else. No posture quietly puts a stranger in one of these groups — an open app lets a signed-in visitor write because signed-in is in its posture, not because signing in made them an editor. A group has exactly the people you decided to put in it.

You are always in every group; the owner never needs listing. Add someone to the workgroup by giving them access in the Share card's Share settings, and every pin that names their group picks them up. Remove them, and it drops. Membership changes are a sharing decision, not a configuration change.

The groups nest, widest first: anyone contains signed-in, which contains members, which contains editors, readers and submitters.

What a pin looks like

Each database in the app can be pinned independently, and each of read and write is optional:

{ read: ["editors"], write: ["editors"] }   // fully internal
{ write: ["editors"] }                      // editors write, reads follow the posture
{ write: ["members"] }                      // anyone you invited can contribute

Leaving a capability out is meaningful: that capability is whatever the posture already said. Naming it intersects your groups with the posture's — the result is the narrower of the two, never the wider.

There is no separate delete permission. Deleting a document is a write, and is gated by write like any other.

One useful consequence

Pinning read on a database narrows it below the app's posture for that database specifically. An app can be published openly and still keep one database internal — a public front with a private back — because the pin subtracts from the posture's anyone.

The reverse is not available, and deliberately so: a pin on a private app cannot open one database to the world. An app with one world-facing database is honestly an open app whose access function routes the rest to channels strangers are not in.

One honest exception: your own admin mode passes every pin. If you test a read pin while acting as the deployment's admin, it will look like it isn't working — check with a non-admin account instead.

That also means the reverse mistake is worth naming: filtering documents by author in the app's UI is view-scoping, not access control. If the data is reachable, it is reachable. Pin the database.

Where this does not reach

A few things are deliberately governed elsewhere, and a pin will not change them:

  • Platform databases — the ones whose names start with an underscore, like the built-in _comments — are the platform's, not your app's. Your access.js cannot route them and cannot claim their names. Comments still sit under your posture's reading rule, so a private app does not have world-readable comments.
  • Direct messages are governed by the platform's own DM rules. No app setting opens someone else's DMs.
  • Component media databases are governed by a fixed built-in rule, so a pin stored against one is ignored.
  • Signed-out visitors need two yeses, not one. Your access function has to accept an anonymous write, and the deployment's own anonymous setting has to admit anonymous writers — the function's declaration is a proposal, and the setting is what ratifies it. Without an access function, an anonymous write is refused however the setting is left.

And the standing rule for anything you turn off: revoking access stops someone receiving your data from that point on. It does not reach back and erase what already synced to their device. See Local-first data and sync for what that means in practice.

Setting one today

Being straight about the current state: the posture is yours to set, and the per-database pins mostly are not yet.

The posture, from the command line. push takes the posture as one word:

npx vibes-diy push --access open      # the default: reachable by anyone
npx vibes-diy push --access gated     # anyone can view, members write
npx vibes-diy push --access private   # members only
npx vibes-diy push --private          # the same thing, spelled shorter

The posture, in the browser. Publishing from the web sets it for you — an ordinary app is published open, and an app that declared itself privacy-sensitive is published gated instead. After that, the Restricted | Public switch in the Share card's Share settings is the same setting: Restricted is private, and Public goes back to the published default. If your app's access function offers to take writes from signed-out visitors, publishing it is also where the deployment's anonymous setting is recorded — the function proposes and the publish ratifies.

Per-database pins are still thin. There is no per-database control in the Share card and no CLI command for one; pinning a database means calling the app-settings API yourself. The Share card does not list your pins yet either. An ordinary database you have never pinned is governed by the posture row above, with nothing narrowing it.

If your team needs the general control, that gap is the thing to ask for — not a workaround in the app's code. A pin you can see and change in settings is the point; the same restriction hand-rolled into access.js is a fork you now maintain.

Where to go next

  • The access model — accounts, handles, channels, grants and roles, and why your own alt handle is treated like a stranger
  • Access API — the access.js contract itself
  • Sharing & Access — the grant states and the Share card
  • Developer grants — delegating code changes without handing over ownership