Privacy and sharing
Every app is private by default: one owner, one dataset, and you decide who else gets in and what they can do.
What you can do
Private by default. A new app has one owner and one shared dataset. Sign in from your phone or another computer to open it securely from anywhere. Nobody else can open it until you invite them or make it public.
Invite people. Share the app with specific people and pick what each can do: edit it, use it, or just view it. Change or revoke someone’s access at any time. Need more control? Contact us.
Make it Public, deliberately. A separate, explicit step lets anyone with the URL open the live App Viewer with no sign-in. Public visitors can read but cannot change data. If you also want anyone who signs in to Charming, not just people you invited, to read and write the App’s data, choose that wider general access level. Turn either off again at any time.
Choose Template and Listed separately. Template lets people who can already view the source App make an independent copy. Listed adds public discovery and copying without granting access to the live App or its data. Listed always includes Template. See Templates for listing content and copy behavior.
How access works
Every app sits at one of three general access levels, from narrowest to widest:
- Invited people only. Nobody else can open the app.
- Everyone with the link can view. No sign-in needed; visitors can look but not change data.
- Everyone can view, signed-in people can edit. Level 2, plus: anyone who signs in (not just people you invited) can also read and write app data.
Invites are additive on top of whichever level you pick: an invited viewer, end-user, or collaborator keeps their access even at “Invited people only.” An App set up before these three levels existed may sit outside all three, such as signed-in access without a Public link. The owner is asked to pick one of the three the next time they open Sharing settings.
The owner or a team admin can invite people, change what they can do, or change the general access level. The Public link stays view-only on its own. Signed-in write access, an end-user invitation, or a collaborator invitation grants write access.
Public, Template, and Listed answer different questions:
| Setting | What it grants | What it does not grant |
|---|---|---|
| Public | Anyone with the link can open the live hosted App Viewer as a read-only viewer. | Source editing, data writes, Template copying, or public discovery. |
| Template | Existing readers of the source App can make an independent copy. | Access for strangers, a public directory entry, or Public App access. |
| Listed | Anyone can discover the Template and make a copy. Listed implies Template. | Access to the live source App or its data. |
Technical
Account Home lists the Apps you own and the ones shared with you. To change or revoke access without an agent, open the App’s App settings, then choose Sharing. Use the separate Template section for Template state, listing content, and the starter prompt.
Copy this prompt for your agent
Set up sharing for my Charming app. Read
https://charm.ing/docs/capabilities/privacy-and-sharing.md first.
Suggest who it makes sense to share it with and what each person
should be able to do, then ask me to reply with email addresses.
How an agent performs this job
Use share_app to invite someone, list_app_shares to inspect access, and unshare_app to revoke it. The tool pages define inputs and errors.
Use set_public or unset_public to change Public access. These tools only change the live App Viewer link. They do not change Template or Listed. An anonymous App must be claimed first.
The contract
Access is enforced server-side: every route declares whether it’s read-only, and the platform denies a role that can’t attempt an op regardless of what the app’s code does. Roles stack in one direction only. collaborator can do everything end-user can, which can do everything viewer can:
- Owner. Full control: edit source, run any op, share, unshare, delete, make Public, edit Template settings, and transfer.
collaborator. Open, edit source, run the App, and edit Template settings, short of sharing, deleting, or transferring it.end-user. Open the app and run it, including writes to its data, but can’t see or change the app’s source.viewer. Open and run read-only operations only. A well-built app hides or disables its write actions for a viewer instead of letting them fail.- Public (no login). Same access as
viewer, and only after the owner or a team admin turns it on. Letting signed-in visitors write without an invite needs the widest general access level: see How access works.
Team Apps and ownership transfer
Create a team with POST /api/v1/teams, then manage its members in Account settings → Teams. An MCP agent can create a team-owned App by passing team_id to create_app. Only a team owner or admin can create an App there. Omitting team_id creates a personal App.
Team owners and admins set defaults under Team settings → Apps, or with GET and PATCH /api/v1/teams/{teamId}/app-defaults. The defaults choose one general access level and whether Template starts enabled. With no saved defaults, new team Apps start at Invited people only with Template off. Template-on never means Listed.
Charming snapshots those defaults when it creates an App in the team or transfers a personal App into it. Changing the team defaults does not alter existing team Apps, and updating an existing App does not reapply them. A transfer into a team can therefore change general access, turn Template on or off, and unpublish Listed when the destination default turns Template off. A transfer from a team to your personal account keeps the App’s current access and Template state.
Move an App with POST /api/v1/apps/{appId}/transfer. A transfer changes its owner and friendly URL; the old live App URL redirects to the new one. Its storage, secrets, source, and saved Template listing draft move with it. Existing direct App members, pending invitations, and active single-use links stay by default. If the transfer sets keepExistingAppMembers: false, Charming deletes direct grants and pending invitations and revokes active single-use links. Team membership access follows the owner: destination team members gain their team role, while a move to your personal account leaves only you with owner-level management unless a direct grant remains.
Building a role-aware UI
Use window.charming.viewer.can(op) to hide write actions from read-only viewers, after checking window.charming.user. An anonymous visitor on an app open to signed-in editors also reads can(op) === false, and hiding the button there is what stops them ever reaching the sign-in. Hide the action only for a caller who is signed in and still refused, or where charming.login.available is false; otherwise leave it live and route the click through withUser below. The server still enforces access. See the design prompt for the build rules and the Browser runtime API for viewer, user, login, and withUser.
Signing a visitor in mid-app
An app open to signed-in visitors can start the sign-in itself with window.charming.login(), and the caller it returns is live: charming.user, charming.viewer.role, and gated calls all reflect the new session with no reload.
Wrap the gated action in window.charming.withUser(action) rather than asking someone to sign in first. It runs the action, signs them in only if the action turns out to need it, and runs it again with the payload the closure captured, so a visitor’s half-finished work is saved rather than thrown away. Wrap the call, not the whole click handler: withUser re-runs the closure verbatim, so anything else inside it happens twice.
try {
const saved = await charming.withUser(() => api.saveScore({ score }));
if (saved === null) status.textContent = 'Not saved. Sign in when you are ready.';
} catch (err) {
if (err.name !== 'CharmingLoginError' || err.reason !== 'popup-blocked') throw err;
signInButton.hidden = false; // its own click handler calls charming.login()
}
withUser resolves null only when the visitor closes the sign-in window, so have the wrapped op return something other than null — otherwise a save that worked reports itself as a cancel.
withUser retries around two error kinds. sign_in_required (401) is the server’s answer to a gated call from a visitor with no account. forbidden is the runtime’s own viewer gate, which refuses a write before any request leaves the page. An anonymous visitor hits forbidden first, so branch on both if you handle this by hand rather than through withUser.
A browser can refuse the sign-in popup instead of opening it — login() then rejects with a CharmingLoginError carrying reason: 'popup-blocked', not the null a cancel resolves with. withUser’s own retry is the likely place to hit this: its login() call happens after the wrapped action has already failed once, so the popup no longer opens inside the click that started it.
Inside a chat-host embed, charming.login.available is false: withUser opens no popup and rethrows the refusal the action raised. Check that flag before you offer the gated action at all, and render a link to charming.login.canonicalUrl in its place. See Gate an action behind sign-in for the full pattern, including how to recover from a blocked popup.
Limits, access, and deletion
- Sharing requires a claimed app. An anonymous app has no owner yet, so
share_appandset_publicfail until you claim it. - Ownership can’t be shared away. Sharing, revoking, deleting, transferring, and making an app public or private stay owner-only regardless of anyone else’s role.
- Public is read-only on the main URL. Anonymous visitors can read the App’s shared data but cannot run mutating operations. A confirmed custom domain uses a separate per-visitor storage policy. Turning Public off does not delete existing App data.
- Revoking access is immediate for new requests. An already-open page can keep working on its short-lived credentials for a short window after you revoke.
Related
- Gate an action behind sign-in: the full mid-app sign-in pattern, including a blocked popup
- Templates: let existing App readers copy, or publish a Listed Template for anyone
- Custom domains: give public visitors their own private data instead of one shared pool
- Data storage
- Connect to any AI agent: roles and access apply no matter which agent connects to the app
- Browser runtime API
- How Charming works
- Docs home
- llms-full.txt