---
name: post-permissions-and-viewing
description: Explain, diagnose, or change Vmax post permissions and viewing. Use for Public access partner domains, individual waitlist admission, unpublished team previews, personal drafts, publication, and team URLs across the website and sibling internet.dev API.
---

# Post permissions and viewing

Use this skill for Vmax only. Public access is a special case in the shared API;
it must not change another project's authentication, waitlist, or post permissions.
The same skill is served at `https://vmax.ai/skill/permissions` and linked from both
workspaces. Read current source before changing a permission.

## Permission model

| Account or grant | Sign in to Vmax | Read unpublished team posts | Read someone else's personal draft | Manage Public access |
|---|---|---|---|---|
| Verified `internet.dev` or `vmax.ai` account | Yes | Yes | Yes, for Vmax posts | Yes, permanent Vmax admin |
| Verified account on an accepted domain | Yes | Yes | No | No |
| Individually accepted waitlist account | Yes | No | No | No |
| Everyone else | No | No | No | No |

Ownership remains separate: a post's author can read their own draft and keeps
the existing owner editing permissions. Published posts remain public. API
updates require a verified owner or the existing platform-admin write grant;
Public access does not grant editing, deletion, publication, organization, or
waitlist administration rights. The website editor retains its own owner gate.

Permanent Vmax admins are authenticated, verified accounts whose exact email
domain is `internet.dev` or `vmax.ai`. They cannot be removed through Public
access. This is application authority, not a change to the shared API's account
tiers: do not promote these accounts to global ADMIN merely for this feature.
An unrelated platform administrator does not gain Public access management.

An accepted partner domain grants all verified accounts on that exact domain
sign-up access and previews of Vmax posts with `data.teamPost === true`. It does
not grant access to personal drafts, another product's private posts, or another
user's draft list. There is no per-person, per-article, or expiring domain grant.
If the request is narrower than the available grant, resolve that scope first.

`document.llc` and `modal.com` are the initial managed partner domains. Both may
be removed and re-added. `internet.dev` and `vmax.ai` are permanent admins outside
the editable table. The database seeds partners only on first creation, so a
later migration cannot resurrect a revoked domain.

Removing a partner domain revokes its team-preview permission on the next read.
Someone with a separate accepted waitlist entry still has app access, but loses
team previews. Removing domains does not delete accounts or work. Revoking an
individual waitlist entry does not cancel an independent accepted-domain grant.

## Authoritative files

The website is `demiurge/public/www`. The sibling API checkout is
`Development/apis`, or `../../../apis` from the website directory. Read its
`AGENTS.md` before editing API files.

| Decision | Website owner | API owner, relative to `apis/` |
|---|---|---|
| Fixed Vmax admin domains | `document-system-common/constants.ts`, `viewer-access.ts` | `data/public-access.ts`: `PUBLIC_ACCESS_STAFF_DOMAINS`, `isPublicAccessStaff` |
| Partner grants | `FormPublicAccess`, `HTTP.publicAccessDomains` | `public_access_domains`, `publicAccessRepository`, `common/public-access-api.ts` |
| Individual app admission | `Server.admitViewer`, `needsPublicAccessCheck` | Vmax waitlist rows, read through `/api/public-access/status` |
| Team-preview capability | `Server.setup.canPreviewTeamPosts`, passed to both page and metadata reads | `publicAccessRepository.canPreviewTeamPosts`, resolved from the authenticated reader |
| Post reading | `next-slate-markdown/common/post-access.ts`, `Server.getPostBySlug` | `common/post-access.ts`, `common/post-reader.ts`, both single-post endpoints |
| Editing and publication | Owner editor, `EditablePostWithLayout` | Existing `pages/api/posts/update.ts` and `delete.ts` |
| Team attribution | `post-path.ts`, `PostTitle` | Stored `posts.data.teamPost` |

`PUBLIC_ACCESS_SITE` is fixed to `vmax.ai` on the server. Every domain query and
mutation includes that site. A request explicitly naming another `source` is
rejected. The new policy is consumed only by Vmax's website admission and
`NEXT_SLATE_WORLD_MARKDOWN` team-post reads. Shared OAuth, account creation,
existing waitlist endpoints, and other products' privacy semantics remain intact.

## Add, inspect, or remove a partner

Use **Public access** in the signed-in sidebar (`/public-access`), visible only to
verified `internet.dev` and `vmax.ai` accounts. Other domains, including accepted
partners, do not see this item. The page uses
the waitlist/post-list row layout and inline actions, the shared domain input,
and the existing confirmation and error modals. Follow the
existing website authentication pattern: browser actions read the `vmax` cookie
and the shared HTTP client supplies its value as `X-API-KEY` to `api.internet.dev`.
The server-rendered page uses the same client with its resolved session key.
There is no Public access proxy or separate session scheme. The `app-interface`
reference renders the same component on local fixtures without API writes.

| Endpoint | Permission | Result |
|---|---|---|
| `POST /api/public-access/list` | Permanent Vmax admin | Saved partners plus the two non-removable admin domains |
| `POST /api/public-access/add-domain` | Permanent Vmax admin | Add an exact domain; return the refreshed list |
| `POST /api/public-access/remove-domain` | Permanent Vmax admin | Remove an exact partner domain; return the refreshed list |
| `POST /api/public-access/status` | Authenticated account, self only | `accepted`, `canPreviewTeamPosts`, and the domain/waitlist admission reason |

The table describes endpoints on `api.internet.dev`. All four are unlimited and
have zero token cost, with API-key authentication and Vmax-only authorization
retained. Opening the raw API URL in the address bar does not supply the header;
the signed-in website supplies it through the same client as its other endpoints.

Management writes take `{ "domain": "partner.example", "source": "vmax.ai" }`
and the admin's own `X-API-KEY`. Identity never comes from a body email or the
editable domain table. Status never accepts a different viewer from the caller.

Domains are trimmed, lowercased, and matched exactly. Use ASCII DNS names,
including punycode when needed. Reject email addresses, URLs, paths, ports,
wildcards, malformed labels, and IP addresses. Subdomains need their own grant;
`person@notpartner.example` and `person@partner.example.evil.test` do not match
`partner.example`. Duplicate adds and repeated removals are idempotent.

Use the existing user authorization when performing the requested change; do not
ask for the same permission again. A skill-writing task does not itself authorize
new live grants. Keep example domains out of the live table unless requested.
Do not change static staff registries to onboard partners, alter shared account
tiers, or publish a draft to make previewing work.

## Operational access and rollout

For an authorized live inspection or correction, use the API workspace's existing
environment variables before asking the user for credentials. `data/db.ts` names
the `API_DATABASE_*` settings and SSL behavior. Keep credentials out of command
arguments, logs, documentation, and client props. Query only the needed records.
Database operator access is not evidence of a normal viewer's permission.

Deploy the API migration and routes before switching the website admission
caller. In `apis`, `npm run migrate-public-access -- --dry-run` describes the
migration; `npm run migrate-public-access` applies it with existing database
settings. Both standard bootstrap paths also run it. The dedicated table and
one-time Vmax partner seed leave existing users, organizations, posts, and other
projects unchanged. The migration is transactional, serialized, and repeatable.
Follow the user's deployment scope; local code does not prove production state.

## Verification

Check both `/api/posts/{id}` and `/api/posts/public/{slug}`, then document and
paper routes including metadata. Public posts stay public; unpublished content
requires its owner, a verified permanent Vmax admin, or a verified accepted-domain
reader when the post is a Vmax team post. A missing publication flag is a draft;
`private: true` blocks anonymous access even when `public: true` is also present.

Verify domain removal while the reader retains an individual waitlist approval:
app admission must stay true, team-preview capability must become false, and both
post endpoints must deny the team draft. Check personal drafts, other post types,
domain lookalikes, unverified accounts, repeated keys, and another site's domain
records. Partners must never be able to manage the list.

Private API responses use `Cache-Control: private, no-store`; website reads bypass
caches and apply the same permission to body and metadata. A failed admission
lookup denies access. `customPlainTextPassword` is a client presentation gate,
not an API permission; do not use it to protect or grant partner previews.

Run the relevant existing checks in each workspace:

```sh
npm run check
npm test -- next-slate-markdown/common/viewer-access.test.ts next-vmax-tools/FormPublicAccess.test.tsx
```

```sh
node_modules/.bin/tsc --noEmit --incremental false --pretty false
npm test -- common/public-access.spec.ts common/post-access.spec.ts common/server.spec.ts
npm run test-public-access-integration
```

Integration tests use temporary PostgreSQL and synthetic accounts, never live
credentials. Follow the active task's browser instructions. Report code changes,
migrations, deployments, and live verification separately.

## Team attribution and publication

`teamPost: true` chooses `/team/{slug}` and its `/paper` variant, and makes the
unpublished post eligible for accepted-domain previews. It does not publish it.
The editor shows Team URL On/Off beside its production URL; the Vmax-logo toolbar
control changes it, separately from the public/private control.

For an authorized change, send only the intended field through the owner/admin
update endpoint:

```json
{"id":"POST_ID","updates":{"data":{"teamPost":true}}}
```

The endpoint shallow-merges `data`. For an authorized database correction, use
an atomic field update on the exact record and verify the result. Preserve the
body, slug, owner, other metadata, and publication flags; do not replay an old
whole-post snapshot.

For serialization, read the [posting guide](../../public/SKILL.md). Implementation
invariants remain in [next-slate-markdown/AGENTS.md](../../next-slate-markdown/AGENTS.md).
