diff --git a/frontlit/docs/_sections.json b/frontlit/docs/_sections.json
index 9a6275e..c7dba35 100644
--- a/frontlit/docs/_sections.json
+++ b/frontlit/docs/_sections.json
@@ -1,4 +1,10 @@
{
"version": 1,
- "sections": []
+ "sections": [
+ { "folder": "getting-started", "name": "Getting Started", "order": 10 },
+ { "folder": "teams", "name": "Teams", "order": 20 },
+ { "folder": "self-hosting", "name": "Self hosting", "order": 30 },
+ { "folder": "components", "name": "Components", "order": 40 },
+ { "folder": "developers", "name": "Developers", "order": 50 }
+ ]
}
diff --git a/frontlit/docs/components/page-builder.md b/frontlit/docs/components/page-builder.md
new file mode 100644
index 0000000..21ab14a
--- /dev/null
+++ b/frontlit/docs/components/page-builder.md
@@ -0,0 +1,137 @@
+---
+title: Page builder
+description: Install and use the block-based WYSIWYG page builder and renderer.
+nav_order: 10
+---
+
+`@frontlit/page-builder` is a block-based page builder and renderer for React. It is self-contained — no backend
+and no framework lock-in. Themes, blocks, and the WYSIWYG builder all run client-side.
+
+A page is a `Theme` plus a `WidgetInstance[]` layout. Each widget is one block from the registry (hero, header,
+footer, pricing, faq, grid, rich-text, email-form, and more).
+
+Rich text editing and rendering live in [`@frontlit/text-editor`](/docs/components/text-editor), which this package
+depends on and installs automatically.
+
+To try the builder, install the package and render `PageBuilder` as shown in [Usage](#usage), or run the [components demo](https://github.com/codelitdev/frontlit/tree/main/examples/components-demo) from the FrontLit repository.
+
+## Installation
+
+```bash
+npm i @frontlit/page-builder
+# or
+pnpm add @frontlit/page-builder
+```
+
+Peer dependencies: `react` and `react-dom` (^18 or ^19). Next.js is **not** required.
+
+### CSS
+
+Import the compiled stylesheet. It includes the utilities, default tokens, and the theme-picker fonts:
+
+```ts
+import "@frontlit/page-builder/styles.css";
+```
+
+If the host already runs Tailwind CSS v4, you can skip `styles.css` and scan the packages instead. Import the
+prose sheet so rich-text lists and code blocks survive Tailwind preflight:
+
+```css
+@import "@frontlit/page-builder/theme.css";
+@import "@frontlit/text-editor/prose.css";
+@source "./node_modules/@frontlit/page-builder";
+@source "./node_modules/@frontlit/text-editor";
+```
+
+## Usage
+
+Both `PageRenderer` and `PageBuilder` are client components (the package ships with a `"use client"` directive on
+UI entries).
+
+```tsx
+import {
+ PageRenderer,
+ PageBuilder,
+ createDefaultLayout,
+ getDefaultTheme,
+} from "@frontlit/page-builder";
+import "@frontlit/page-builder/styles.css";
+
+// Read-only rendering (a published page, a marketing site, a preview):
+;
+
+// The interactive editor — persist however you like:
+ {
+ /* save */
+ }}
+/>;
+```
+
+`PageBuilder` holds all editor state in memory and calls `onChange` on every edit. The host app decides how (and
+whether) to persist it.
+
+```tsx
+import { PageBuilder, type PageBuilderState } from "@frontlit/page-builder";
+
+function Editor({ savedPage }: { savedPage: SavedPage }) {
+ function handleChange(state: PageBuilderState) {
+ // Persist state.layout / state.theme / state.seo, e.g. via
+ // `updatePage(pageId, { layout: state.layout, ... })`.
+ }
+
+ return (
+
+ );
+}
+```
+
+`header` and `footer` are required layout-level blocks. The builder repairs legacy or empty initial layouts by
+adding them, keeps them at the outer edges, and never permits moving or deleting them.
+
+### Client-side routing
+
+Internal `/...` links render as native `` tags by default, so the package works in Vite, Remix, Astro, or any
+other React host. Next.js (or any other router) can inject its own link:
+
+```tsx
+import Link from "next/link";
+import {
+ PageRenderer,
+ type PageBuilderLinkProps,
+} from "@frontlit/page-builder";
+
+function AppLink(props: PageBuilderLinkProps) {
+ return ;
+}
+
+;
+```
+
+`PageBuilder` accepts the same `linkComponent` prop and forwards it to the live preview.
+
+## What's inside
+
+| Subpath | What it exports |
+| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
+| `@frontlit/page-builder` | Everything below, re-exported from one place |
+| `@frontlit/page-builder/models` | `Theme`, `WidgetInstance`, `Widget`, `Page`, `Media`, etc. (re-exports `TextEditorContent` from `@frontlit/text-editor`) |
+| `@frontlit/page-builder/primitives` | Themed UI atoms (`Button`, `Header1-4`, `Section`, `PageCard`, ...) + built-in themes |
+| `@frontlit/page-builder/components` | Shared presentational bits: `Image`, `NavLink`, `VideoWithPreview` (re-exports `TextRenderer` from `@frontlit/text-editor`) |
+| `@frontlit/page-builder/blocks` | The block library: Header, Hero, Content, Featured, Faq, Pricing, Banner, EmailForm, Footer, … |
+| `@frontlit/page-builder/renderer` | `PageRenderer` — renders a page's `WidgetInstance[]` layout, with an `editing` mode for use inside the builder |
+| `@frontlit/page-builder/builder` | `PageBuilder` — the full WYSIWYG editor (add/remove/reorder blocks, edit settings, switch themes) |
+| `@frontlit/page-builder/data-slots` | Server-safe helper for hosts to resolve named data slots before rendering |
+
+`createDefaultLayout()` / `getDefaultTheme()` (exported from the root) build a ready-to-render starter landing
+page.
diff --git a/frontlit/docs/components/text-editor.md b/frontlit/docs/components/text-editor.md
new file mode 100644
index 0000000..86b7d4f
--- /dev/null
+++ b/frontlit/docs/components/text-editor.md
@@ -0,0 +1,133 @@
+---
+title: Text editor
+description: Install and use the TipTap-based rich text editor and static renderer.
+nav_order: 20
+---
+
+`@frontlit/text-editor` is a TipTap-based rich text editor and static renderer for React. It is
+framework-agnostic: it depends only on `react` / `react-dom`. No Next.js, no backend, and no media-upload
+service.
+
+Uploaded images are embedded as data URLs by default. Hosts can replace that with their own picker via
+`ImagePickerContextProvider`.
+
+The page builder's rich-text block uses this package. You can also use it on its own — for example in a
+dashboard form, a comment field, or a help-center article editor.
+
+To try the editor, install the package and render `Editor` as shown in [Usage](#usage), or run the [components demo](https://github.com/codelitdev/frontlit/tree/main/examples/components-demo) from the FrontLit repository.
+
+## Installation
+
+```bash
+npm i @frontlit/text-editor
+# or
+pnpm add @frontlit/text-editor
+```
+
+Peer dependencies: `react` and `react-dom` (^18 or ^19).
+
+### CSS
+
+Import the compiled stylesheet. It includes Tailwind utilities, fallback tokens for the editor chrome,
+ProseMirror/renderer rules, and a small highlight.js theme. Tailwind is **not** required in the host:
+
+```ts
+import "@frontlit/text-editor/styles.css";
+```
+
+If you already run Tailwind CSS v4 and want to generate the utilities yourself, skip `styles.css`, scan the
+package, and import the prose rules (lists, blockquotes, code blocks — these are not utilities):
+
+```css
+@import "@frontlit/text-editor/tokens.css";
+@import "@frontlit/text-editor/prose.css";
+@source "./node_modules/@frontlit/text-editor";
+```
+
+## Usage
+
+`Editor` is a client component. Persist the TipTap JSON it emits, then render that JSON with `TextRenderer`.
+
+```tsx
+import { Editor, TextRenderer, emptyDoc } from "@frontlit/text-editor";
+import "@frontlit/text-editor/styles.css";
+
+ save(json)} />;
+;
+```
+
+A typical host component:
+
+```tsx
+"use client";
+
+import { useState } from "react";
+import {
+ Editor,
+ TextRenderer,
+ emptyDoc,
+ type TextEditorContent,
+} from "@frontlit/text-editor";
+import "@frontlit/text-editor/styles.css";
+
+export function ArticleBody({
+ initial,
+ onSave,
+}: {
+ initial?: TextEditorContent;
+ onSave: (json: TextEditorContent) => void;
+}) {
+ const [doc, setDoc] = useState(initial ?? emptyDoc);
+
+ return (
+ <>
+ {
+ setDoc(json);
+ onSave(json);
+ }}
+ />
+
+ >
+ );
+}
+```
+
+### Custom image picker
+
+By default the editor embeds uploaded images as data URLs. Wrap the editor with
+`ImagePickerContextProvider` to open your own media library instead:
+
+```tsx
+import { Editor, ImagePickerContextProvider } from "@frontlit/text-editor";
+
+ {
+ const media = await openMediaLibrary();
+ if (!media) return null;
+ return { src: media.url, alt: media.alt };
+ }}
+>
+
+;
+```
+
+### Theming the renderer
+
+`TextRenderer` accepts an optional, loosely-typed `theme` prop (`TextRendererTheme`) to style headings,
+paragraphs, and links. Without one it falls back to plain semantic HTML tags. It is structurally typed against
+that small theme shape instead of importing `@frontlit/page-builder`, so this package stays usable on its own.
+
+## What's inside
+
+- `Editor` — the WYSIWYG editing surface (toolbar, bubble menu, tables, code blocks with syntax highlighting,
+ images, links).
+- `TextRenderer` — renders saved `TextEditorContent` (TipTap JSON) as static React elements.
+- `TextEditorContent` — the TipTap doc-JSON type both of the above operate on.
+- `normalizeTextEditorContent`, `extractTextFromTextEditorContent`, `truncateTextEditorContent` — content
+ utilities (e.g. for building excerpts).
+- `emptyDoc`, `createExtensions`, `createId`, `extractHeadings`, `extractTextFromNode` — lower-level building
+ blocks, exported for advanced use.
+- `ImagePickerContextProvider` — optional hook so a host can supply its own image-upload UI.
diff --git a/frontlit/docs/developers/authentication.md b/frontlit/docs/developers/authentication.md
new file mode 100644
index 0000000..00d9703
--- /dev/null
+++ b/frontlit/docs/developers/authentication.md
@@ -0,0 +1,67 @@
+---
+title: Authentication
+description: Authenticating REST and MCP requests against the FrontLit API.
+nav_order: 20
+---
+
+The API recognizes three kinds of authentication, resolved in this order:
+
+1. **OAuth bearer token** — issued by FrontLit's OAuth authorization server for
+ a specific resource, client, user, and explicit scope set. A token belongs to
+ your account and works in every team you're a member of.
+2. **Session cookie** — set by signing in through the web app (passwordless email OTP, via
+ [better-auth](https://www.better-auth.com)). Used by the dashboard itself; not applicable to server-to-server
+ callers.
+3. **API key** — a team-scoped secret, sent as the `x-frontlit-apikey` header (or `apikey` body field). Create one
+ from **Team settings → API keys** in the dashboard, or via `POST /teams/:teamId/keys`. The full secret is only
+ returned once, at creation time — store it securely.
+
+```bash
+curl https://api.frontlit.dev/pages \
+ -H "x-frontlit-apikey: fl_live_..."
+```
+
+## Team scope
+
+Session and OAuth requests choose the team they act on with the `X-Frontlit-Team-Id` header (the dashboard sends
+this automatically once you've picked a team). If an account belongs to exactly one team, the header can be
+omitted. An API key always authenticates as exactly one, fixed team — no header needed, and a header naming another
+team is rejected.
+
+```bash
+curl https://api.frontlit.dev/pages \
+ -H "Authorization: Bearer $TOKEN" \
+ -H "X-Frontlit-Team-Id: team_..."
+```
+
+Use `GET /teams` to list the teams the token can reach. Team selection errors:
+
+| Error | Status | Meaning |
+| --- | --- | --- |
+| `team_required` | 409 | You belong to several teams and sent no header. The body lists your teams. |
+| `no_team` | 409 | The account doesn't belong to any team yet. |
+| `invalid_team_id` | 400 | No team has that ID. |
+| `not_a_team_member` | 403 | You aren't a member of that team (or were removed). |
+| `team_mismatch` | 403 | An API key was used with a team other than its own. |
+
+Access follows membership: a team you join later is reachable with the same token, and leaving a team stops access
+on the next request.
+
+For a human member, team permissions and OAuth scopes are separate grants.
+Browser requests require the member's current team permission. OAuth requests
+require both that permission and the token's scope, so a client cannot exceed
+the authority of the person who connected it. See [Team members and
+permissions](/docs/teams/members) for the dashboard workflow.
+
+## MCP
+
+The remote MCP endpoint accepts an OAuth bearer token or
+`x-frontlit-apikey`. It intentionally rejects dashboard session cookies. See
+[MCP connections](/docs/developers/mcp) for protocol, client, scope, and
+troubleshooting details.
+
+## Provisioning
+
+`POST /provisioning/teams` is a separate, server-to-server-only endpoint for multi-tenant consumers. It's guarded
+by a shared secret (`X-Frontlit-Provisioning-Secret`), not an API key — see the [API Reference](https://api.frontlit.app/docs/)
+for its request/response shape.
diff --git a/frontlit/docs/developers/mcp.md b/frontlit/docs/developers/mcp.md
new file mode 100644
index 0000000..a7c68b4
--- /dev/null
+++ b/frontlit/docs/developers/mcp.md
@@ -0,0 +1,131 @@
+---
+title: MCP connections
+description: Connect OAuth and API-key clients to FrontLit's stateless MCP server.
+nav_order: 30
+---
+
+FrontLit exposes one remote MCP endpoint:
+
+```text
+https://api.frontlit.app/mcp
+```
+
+The endpoint supports the stateless MCP `2026-07-28` protocol and older
+Streamable HTTP clients through the official SDK 2 stateless compatibility
+mode. FrontLit does not expose the retired standalone SSE transport: `GET
+/mcp` is intentionally not a connection endpoint, and no client receives an
+`Mcp-Session-Id`.
+
+## OAuth
+
+OAuth-capable clients should be given only the HTTPS `/mcp` URL. They discover
+the protected resource at:
+
+```text
+/.well-known/oauth-protected-resource/mcp
+```
+
+That document points to FrontLit's authorization server. Current clients can
+onboard through a Client ID Metadata Document (CIMD); Dynamic Client
+Registration remains available for clients such as MCP Inspector. Both use
+authorization code with PKCE, resource binding, user consent, and explicit
+tool scopes.
+
+An OAuth connection belongs to your account, not to one team. There is no
+team picker: one connection works in every team you're a member of, including
+teams you join later, and each call is limited to your role and permissions in
+that team. Leaving a team stops the connection's access to it on the next call.
+
+Team-scoped tools take a `teamId` argument. Call `list_teams` to see your teams
+with your permissions in each, then pass the chosen `teamId`. It is optional
+only when you belong to a single team; otherwise the tool returns
+`team_required` with the list of teams, and the assistant should ask you which
+one to use. Results of tools that change something end with a line naming the
+team they acted in.
+
+Tickets, contacts, documentation improvements, and GitHub proposals contain
+text written by people outside your team. FrontLit marks those tools as
+untrusted so assistants treat their text as data, not instructions. Review
+what an assistant plans to change, especially when it has been reading
+outside-written content.
+
+FrontLit configures an identity-only registration baseline (`openid profile
+email`). Better Auth represents the combined identity and allowed MCP scopes
+as a client's _capability set_, so a DCR response can list every MCP scope.
+That is not an access grant: authorization requests with missing or blank
+`scope` are rejected, and only explicitly requested and approved scopes are
+issued in a token. A consent screen may therefore show many scopes because
+the client requested them.
+
+Scope families include teams, API keys, pages, themes, media, blogs, help
+documents, changelogs, contacts, segments, broadcasts, ESP configuration,
+chatbot, helpdesk, domains, and settings. Read and write scopes are separate,
+and sending a broadcast requires the distinct `broadcasts:send` scope.
+
+## API key
+
+A team API key is a full-team credential until scoped keys are introduced as a
+separate product feature. Configure the client to send:
+
+```text
+x-frontlit-apikey: fl_live_...
+```
+
+For example, clients that accept a JSON header configuration can use:
+
+```json
+{
+ "url": "https:///mcp",
+ "headers": {
+ "x-frontlit-apikey": "fl_live_..."
+ }
+}
+```
+
+The submitted secret is verified at the HTTP boundary and is never copied
+into MCP tool context or logs. API keys cannot call account-only tools such as
+`list_teams`, `create_team`, or `delete_team`. A key only works in its own
+team; a `teamId` naming any other team returns `team_mismatch`.
+
+## Local development
+
+Desktop and hosted clients commonly require public HTTPS. Start the API, then
+use a controlled tunnel or Funnel that proxies the public hostname to the
+local API port. Set `ENABLE_TRUST_PROXY=true` only when the deployment really
+has the configured one trusted proxy hop.
+
+Check each layer before opening a GUI client:
+
+```bash
+curl -i http://localhost:4000/health
+curl -i http://localhost:4000/.well-known/oauth-protected-resource/mcp
+curl -i http://localhost:4000/.well-known/oauth-authorization-server/api/auth
+curl -i -X POST http://localhost:4000/mcp \
+ -H 'content-type: application/json' \
+ -H 'accept: application/json, text/event-stream'
+```
+
+The final request should return `401` with a `WWW-Authenticate` challenge that
+contains the protected-resource metadata URL. Repeat the health and discovery
+checks against the public hostname before starting OAuth.
+
+## Troubleshooting
+
+- If the browser completes OAuth but the client fails, inspect the first
+ subsequent `POST /mcp` status and body.
+- Some clients try `GET /mcp` as standalone SSE after a failed POST. The
+ resulting `404 Cannot GET /mcp` is expected and is usually secondary to the
+ earlier POST failure; do not add SSE to hide it.
+- `no_team` means authentication succeeded but the user has no team yet.
+ Create one with `create_team`, or join a team; no reconnect is needed.
+- `team_required` means you belong to several teams and the call had no
+ `teamId`. Pass one from `list_teams`.
+- `not_a_team_member` means the `teamId` names a team you don't belong to (or
+ were removed from).
+- `invalid_client_metadata` involving Device Authorization or JWT Bearer means
+ the client's published CIMD grants changed or the exact compatibility rule
+ no longer matched. FrontLit does not globally relax grant validation.
+- `ERR_ERL_UNEXPECTED_X_FORWARDED_FOR` means the proxy is forwarding client IPs
+ while Express is not configured to trust that deployment hop.
+- Reconnect after changing scopes, discovery metadata, tool schemas, or the
+ tool catalog because clients may cache them.
diff --git a/frontlit/docs/developers/overview.md b/frontlit/docs/developers/overview.md
new file mode 100644
index 0000000..e3fe4c3
--- /dev/null
+++ b/frontlit/docs/developers/overview.md
@@ -0,0 +1,29 @@
+---
+title: Overview
+description: The FrontLit REST/MCP API — teams, pages, themes, and provisioning.
+nav_order: 10
+---
+
+The FrontLit API is a ts-rest contract (`@frontlit/api-contract`) implemented over REST, documented via OpenAPI,
+and mirrored 1:1 as MCP tools — the same contract and the same underlying queries back all three.
+
+## Concepts
+
+- **Team** — the tenant/data-scope boundary. Every page and theme belongs to exactly one team. An account can
+ belong to several teams.
+- **Page** — a page built with `@frontlit/page-builder`: a `type`, a block `layout`, an optional saved `theme`,
+ and SEO metadata.
+- **Theme** — a named, reusable set of colors/typography/structure settings, shareable across pages.
+- **API key** — a team-scoped secret for server-to-server access (REST or MCP). Shown in full only once, at
+ creation time.
+- **Provisioning** — a server-to-server endpoint (`POST /provisioning/teams`) for multi-tenant consumers to
+ find-or-create one FrontLit team per one of their own tenants, guarded by a shared secret rather than a normal
+ API key.
+
+See [Authentication](/docs/developers/authentication) for how to authenticate requests, and the
+[API Reference](https://api.frontlit.app/docs/) for every endpoint.
+
+When running the API locally, the same contract is also available as Swagger
+UI at [`http://localhost:4000/docs/`](http://localhost:4000/docs/) and
+as raw OpenAPI JSON at
+[`http://localhost:4000/openapi.json`](http://localhost:4000/openapi.json).
diff --git a/frontlit/docs/developers/pages-from-git.md b/frontlit/docs/developers/pages-from-git.md
new file mode 100644
index 0000000..cc9f3fd
--- /dev/null
+++ b/frontlit/docs/developers/pages-from-git.md
@@ -0,0 +1,161 @@
+---
+title: Pages from Git
+description: Keep landing pages in your repository as JSON files and publish them on every push.
+nav_order: 40
+---
+
+Your site's pages can live in your own repository, next to your product code.
+Coding agents and teammates edit them in pull requests; FrontLit publishes the
+folder on every push to your publishing branch.
+
+## Turn it on
+
+Connect GitHub in **Settings → Integrations**. On the folder step, choose the
+**site folder** (default `frontlit`, or empty for the repository root) and tick
+**Landing pages** (untick **Docs** if your docs live elsewhere). Then:
+
+- If `/pages` already has page files,
+ **Check repository** previews the routes, problems and any builder pages that
+ would go offline. **Finish setup** connects and publishes them in one step.
+- If the folder doesn't exist yet, connect anyway, then open the repository
+ dialog and **Export pages**. That opens a pull request adding your current
+ pages, header, footer and theme to the folder. Merging it turns pages sync
+ on. Exported files re-import byte for byte.
+
+An already-connected repository can turn pages on later from the **Pages**
+section of the same dialog.
+
+From then on every page and the site chrome are owned by the repository. In the
+page builder, drafts stay editable and **Publish** becomes **Create pull request**.
+**Stop syncing** hands pages back to the builder; what's live stays live.
+
+## Files
+
+Everything FrontLit syncs lives in one site folder, `frontlit/` by default:
+
+```text
+frontlit/
+├─ site.json # header, footer, theme, redirects, agent instructions (optional)
+├─ docs/ # Markdown docs → /docs/...
+└─ pages/
+ ├─ index.json # /
+ ├─ pricing.json # /pricing
+ └─ blog.json # /blog
+```
+
+- The file name is the URL: lowercase letters, numbers and hyphens.
+ `index.json` is the homepage. Folders inside `pages/` aren't supported yet.
+- `site.json` sits at the root of the site folder. Without it, the current
+ header and footer stay.
+- Some paths are reserved by the site (`/embed`, `/api`, `/schema`, …).
+
+A page file:
+
+```json
+{
+ "$schema": "https://frontlit.app/schema/page.v1.json",
+ "title": "Pricing – Acme",
+ "description": "Simple plans for every team.",
+ "layout": [
+ {
+ "name": "hero",
+ "settings": {
+ "title": "Pricing that *scales* with you",
+ "description": "Start free, upgrade when you **grow**.",
+ "media": { "url": "../images/pricing.png", "alt": "Plans side by side" }
+ }
+ },
+ { "name": "pricing" },
+ { "name": "faq", "settings": { "items": [{ "question": "Can I cancel?", "answer": "Any time." }] } }
+ ]
+}
+```
+
+- `layout` lists blocks top to bottom. A block's `settings` only needs what
+ differs from the block's defaults; the JSON Schema documents every setting and
+ its default.
+- Rich-text settings are Markdown (CommonMark + GFM).
+- Media is `{ "url", "alt" }`. The URL is an `https://` URL, or a path in the
+ repository (no scheme), relative to the file or to the repository root when
+ it starts with `/`; FrontLit uploads the image when it syncs. `http://` and
+ other schemes are rejected.
+- `"published": false` keeps a page as a draft. `"robotsAllowed": false` asks
+ search engines not to index it.
+- The blog, docs and changelog pages hold their feed with
+ `{ "name": "content-feed" }`.
+
+`site.json`:
+
+```json
+{
+ "$schema": "https://frontlit.app/schema/site.v1.json",
+ "header": { "links": [{ "label": "Pricing", "href": "/pricing" }], "ctaLabel": "Sign up", "ctaHref": "/signup" },
+ "footer": { "tagline": "The front office of your SaaS." },
+ "redirects": [{ "from": "/plans", "to": "/pricing" }],
+ "agentInstructions": "Use this site for questions about Acme's API and pricing."
+}
+```
+
+`agentInstructions` is optional Markdown for AI agents: what the site is for
+and when to use it. It shows as its own section of the site's `/llms.txt`.
+
+## Markdown for AI agents
+
+Every site also serves its public content as Markdown:
+
+- Request any page, docs page, blog post or the changelog with
+ `Accept: text/markdown`, or add `.md` to its URL (`/docs/setup.md`,
+ `/index.md` for the homepage). Browsers keep getting HTML.
+- A missing page requested as Markdown returns a 404 with a short Markdown
+ body that links to `/llms.txt` and `/sitemap.xml`.
+- `/llms.txt` lists the site's pages, docs, blog posts, changelog and OpenAPI
+ spec with links to their `.md` URLs. Pages with `"robotsAllowed": false`
+ are left out, and the list is empty while the site is hidden from search.
+
+## How a push is published
+
+- Each push to the publishing branch is published as one **release**: every
+ page plus the header, footer and theme, together with that commit's docs.
+- Validation is all-or-nothing. If any file is invalid, or a header or footer
+ link points to a page that doesn't exist, nothing changes on the live site
+ and the problems (file, JSON pointer, message) show in the repository dialog.
+ Missing descriptions, long titles and broken links inside pages are warnings.
+- A push that doesn't touch the pages folder, `site.json` or the docs folder
+ publishes nothing.
+- **Website → Releases** lists releases; activate an older one to roll back.
+- Pull requests that change pages get a **FrontLit / pages preview** check
+ linking to a private, unindexed preview of the result.
+
+## Formatting
+
+FrontLit writes files in one canonical form, so diffs stay small: keys in
+schema order, settings equal to a default left out, 2-space indent and a
+trailing newline. Files you write by hand don't have to match it.
+
+## For coding agents
+
+Install the `frontlit-pages` skill. It teaches agents the blocks, copy and SEO
+rules and validates pages before they open a pull request:
+
+```bash
+npx skills add codelitdev/frontlit-skills --skill frontlit-pages
+```
+
+Or add this to your repository's `AGENTS.md` (or `CLAUDE.md`):
+
+```md
+## FrontLit pages
+
+Marketing pages live in `frontlit/pages/*.json` and publish on merge.
+- File name = URL (`pricing.json` → `/pricing`, `index.json` → `/`). Flat folder only.
+- Every file starts with `"$schema": "https://frontlit.app/schema/page.v1.json"`;
+ validate against it before committing. `frontlit/site.json` (schema
+ `site.v1.json`) holds the header, footer, theme, redirects and agent instructions.
+- A page is `{ title, description, layout: [{ name, settings? }] }`. Only set
+ settings that differ from the block defaults listed in the schema.
+- Rich text is Markdown. Media is `{ "url", "alt" }` with an `https://` URL or
+ a repo-relative image path; repo images are uploaded on sync.
+- One H1 per page (the hero); other block titles are H2.
+- Don't add a header or footer to pages — they come from `site.json`.
+- Header and footer links must point at existing pages, or the release fails.
+```
diff --git a/frontlit/docs/getting-started/introduction.md b/frontlit/docs/getting-started/introduction.md
new file mode 100644
index 0000000..28cf8a1
--- /dev/null
+++ b/frontlit/docs/getting-started/introduction.md
@@ -0,0 +1,14 @@
+---
+title: Introduction
+description: FrontLit is a block-based page builder and REST/MCP API for building and serving pages.
+nav_order: 10
+---
+
+FrontLit is the frontoffice of your SaaS: a block-based WYSIWYG page builder (`@frontlit/page-builder`) paired
+with a REST/OpenAPI/MCP API for managing teams, pages and themes programmatically.
+
+- **[Getting Started](/docs/getting-started/overview)** — set up the API and web app locally.
+- **[Self-hosting](/docs/self-hosting)** — run SendLit and its dependencies with Docker Compose.
+- **[Components](/docs/components/page-builder)** — install and use `@frontlit/page-builder` and `@frontlit/text-editor`.
+- **[Developers](/docs/developers/overview)** — authentication, teams, and API keys.
+- **[API Reference](https://api.frontlit.app/docs/)** — the full REST API, generated from the same contract the server validates against.
diff --git a/frontlit/docs/getting-started/overview.md b/frontlit/docs/getting-started/overview.md
new file mode 100644
index 0000000..bd1d80f
--- /dev/null
+++ b/frontlit/docs/getting-started/overview.md
@@ -0,0 +1,74 @@
+---
+title: Overview
+description: Run the FrontLit API, dashboard, and public sites locally.
+nav_order: 20
+---
+
+FrontLit is a Bun workspace with four apps and several packages:
+
+- `apps/api` — the REST/OpenAPI/MCP API (Express, ts-rest, better-auth, Drizzle/Postgres).
+- `apps/web` — the dashboard (Next.js), authenticated against `apps/api`.
+- `apps/sites` — public tenant sites (Next.js).
+- `apps/docs` — this documentation site (fumadocs).
+- `packages/api-contract` — the shared ts-rest contract (Zod schemas + routes).
+- `packages/page-builder` / `packages/text-editor` — the page builder and rich text editor.
+
+## Start local dependencies
+
+`docker-compose.yml` is the self-host product stack. For development, start
+only Postgres, Redis, and SendLit:
+
+```bash
+docker compose -f docker-compose.local.yml up
+```
+
+Postgres is on `127.0.0.1:5433`. SendLit is on `http://localhost:3001`.
+Redis is on `127.0.0.1:6379` (used by SendLit inside Compose). The
+`frontlit-migrate` job applies FrontLit schema to Postgres — wait for
+`FrontLit migrations applied.` before starting the API.
+
+Do not run this at the same time as `docker compose up` (the self-host
+stack) — both publish those ports. See [Self hosting](/docs/self-hosting) for
+the in-Docker product.
+
+On first SendLit boot, copy the one-time organization key into
+`apps/api/.env` as `SENDLIT_ORGANIZATION_API_KEY`:
+
+```bash
+docker compose -f docker-compose.local.yml logs sendlit-api
+```
+
+## Run FrontLit on the host
+
+```bash
+cp apps/api/.env.example apps/api/.env
+cp apps/web/.env.example apps/web/.env
+cp apps/sites/.env.example apps/sites/.env
+# Point DB_CONNECTION_STRING at localhost:5433 (Compose publishes Postgres there).
+bun run --filter @frontlit/api-contract build
+```
+
+Then start the apps in **separate terminals** (`&&` waits for each process
+to exit):
+
+```bash
+bun run dev:api
+bun run dev:web
+bun run dev:sites
+```
+
+Or run all three together:
+
+```bash
+bun run dev
+```
+
+The API listens on `http://localhost:4000`. `GET /health` confirms it's up,
+`GET /docs/` serves the interactive Swagger UI, and `GET /openapi.json`
+serves the raw OpenAPI document. Private content authoring uses typed
+resources below `/content`.
+
+The dashboard listens on `http://localhost:3000`. Signing in sends a
+one-time code to your email (logged to the API's console in development
+instead of actually emailed). Public sites listen on
+`http://localhost:3002`.
diff --git a/frontlit/docs/self-hosting/github-app.md b/frontlit/docs/self-hosting/github-app.md
new file mode 100644
index 0000000..b00e63d
--- /dev/null
+++ b/frontlit/docs/self-hosting/github-app.md
@@ -0,0 +1,213 @@
+---
+title: Set up the documentation GitHub App
+description: Configure your own GitHub App, connect a documentation repository, and verify PRs and publication.
+nav_order: 20
+---
+
+Self-hosted FrontLit uses your own GitHub App to read documentation, open
+setup and edit pull requests, and report validation and publication checks.
+Create one App per deployment. Workspace administrators then connect their
+repositories through **Settings → Content → Source**.
+
+You can use FrontLit's built-in documentation editor without GitHub. This
+App is separate from the personal-token GitHub Releases integration.
+
+## 1. Choose your public URLs
+
+Use an HTTPS API origin reachable by GitHub and the administrator's browser.
+For example:
+
+| Setting | Example |
+| --- | --- |
+| FrontLit dashboard | `https://app.example.com` |
+| FrontLit API | `https://api.example.com` |
+| Authorization callback URL | `https://api.example.com/integrations/github/callback` |
+| Webhook URL | `https://api.example.com/integrations/github/webhook` |
+
+Both GitHub endpoints belong to the **API**, not the dashboard or public
+sites service. Keep the callback URL identical in GitHub and FrontLit.
+
+For local development, use an HTTPS tunnel to the API's actual port. The
+default API port is **4000**:
+
+```bash
+curl http://localhost:4000/health
+tailscale funnel 4000
+tailscale funnel status
+```
+
+Use the tunnel's HTTPS origin for the callback and webhook. Keep
+`GITHUB_DOCS_WEB_ORIGIN=http://localhost:3000` for the local dashboard.
+A tunnel forwarding to an unused port, such as 5000, produces a `502` before
+FrontLit can process the callback. These commands are for local development;
+a deployed reverse proxy should forward the same paths to the API.
+
+## 2. Register the App
+
+In the GitHub account or organization that will own the App, open
+**Settings → Developer settings → GitHub Apps → New GitHub App**.
+
+1. Give the App a unique deployment-specific name and your FrontLit homepage.
+2. Enter the authorization callback URL from step 1.
+3. Enable **Request user authorization (OAuth) during installation**. Leave
+ expiring user access tokens enabled. Use the callback flow; leave the
+ separate Setup URL blank.
+4. Activate webhooks, enter the webhook URL, enable SSL verification, and
+ set a randomly generated webhook secret.
+5. Choose visibility that permits your intended repository owners to install
+ it. A private App is limited to its owning account; choose public when
+ other accounts need to install it. A public App can still be installed
+ on selected private repositories.
+6. Configure permissions and events below, create the App, generate a client
+ secret, and generate/download a private key.
+
+See GitHub's [registration guide](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app).
+
+## 3. Set permissions **and** event subscriptions
+
+Under the App registration's **Permissions & events**, set these repository
+permissions:
+
+| Repository permission | Access | FrontLit use |
+| --- | --- | --- |
+| Contents | Read and write | Read docs; create setup/edit branches and commits |
+| Pull requests | Read and write | Open and track documentation PRs |
+| Checks | Read and write | Report validation and publication outcomes |
+| Metadata | Read-only | Identify the selected repository |
+
+Under **Subscribe to events**, select all three and save:
+
+| GitHub event label | Event name | FrontLit use |
+| --- | --- | --- |
+| Push | `push` | Reconcile the publishing branch after changes or merges |
+| Pull request | `pull_request` | Validate new/updated PRs and track close/merge outcomes |
+| Check run | `check_run` | Handle requested check reruns |
+
+**Permissions and subscriptions are separate.** Granting Pull requests write
+does not enable Pull request events. With only Push selected, a new PR can
+show no FrontLit check until independent PR discovery runs, normally on a
+fifteen-minute schedule. That fallback does not prove webhook delivery.
+Configure subscriptions on the developer-owned **App registration**, not
+just the installation's repository-selection page.
+
+Do not grant Actions, Workflows, organization administration or branch-rule
+bypass for this workflow. Permissions cover the selected repository;
+FrontLit enforces the configured documentation directory within it.
+
+When you add repository permissions to an existing App, its installations
+must approve the upgrade before those permissions take effect. Then review
+the workspace's Git write permissions in FrontLit. See GitHub's
+[App modification guide](https://docs.github.com/en/apps/maintaining-github-apps/modifying-a-github-app-registration).
+
+## 4. Configure FrontLit
+
+For Docker Compose, put these in the root `.env`. The API service's
+`env_file` passes them to the container. For local Bun development, use
+`apps/api/.env` instead.
+
+```dotenv
+GITHUB_DOCS_APP_ID=your_numeric_app_id
+GITHUB_DOCS_APP_SLUG=your-app-url-slug
+GITHUB_DOCS_CLIENT_ID=your_app_client_id
+GITHUB_DOCS_CLIENT_SECRET=your_generated_client_secret
+GITHUB_DOCS_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nYOUR_KEY_CONTENT\n-----END RSA PRIVATE KEY-----\n"
+GITHUB_DOCS_WEBHOOK_SECRET=your_webhook_secret
+GITHUB_DOCS_CALLBACK_URL=https://api.example.com/integrations/github/callback
+GITHUB_DOCS_WEB_ORIGIN=https://app.example.com
+```
+
+Copy the complete generated PEM key; the placeholder above is not a key.
+Literal `\n` escapes in the environment value are supported. The App ID,
+slug, client credentials and private key must all belong to the same App.
+The webhook secret must match GitHub exactly.
+
+Keep these secrets on the API only, outside version control. Do not use
+`NEXT_PUBLIC_*` variables. Set a dedicated `INTEGRATIONS_ENCRYPTION_KEY`
+before storing setup credentials and retain it across restarts. Changing
+that key later requires deliberate credential recovery.
+
+Recreate the API after changing Compose environment values:
+
+```bash
+docker compose up -d frontlit-api
+curl http://localhost:4000/health
+```
+
+For local development, restart your API process. Changing `.env` does not
+update a running process automatically.
+
+## 5. Install and connect a repository
+
+Install the App on the repository-owning GitHub account. Prefer **Only
+select repositories**, select the documentation repository, and save.
+Existing installations also need to approve any pending permission upgrade.
+
+In FrontLit, select the intended workspace and open
+**Settings → Content → Source → Authorize GitHub**. Sign in as a GitHub
+repository administrator, authorize the App, and select its installation,
+repository, publishing branch and documentation directory.
+
+Installation and user authorization are separate: having installed the App
+does not by itself connect a FrontLit workspace. Setup authorization expires
+after ten minutes. Use **Reconnect GitHub** if it expires; an already-created
+setup PR remains available.
+
+For existing repository docs, review the proposed connection. For CMS
+adoption or a new documentation site, review the captured files and choose
+**Create setup PR**. FrontLit opens the PR; there is no manual export or
+commit step. A genuinely empty repository needs an initial placeholder
+commit, disclosed during review, before FrontLit can open its setup PR.
+A missing branch in a nonempty repository must be corrected or replaced
+with an existing branch.
+
+Review any unpublished-draft transfer disclosure: repository readers can
+see draft text exported in the setup PR, including in a private repository.
+
+Merge the setup PR in GitHub. FrontLit tracks its outcome, verifies the
+captured content, and presents **Finish setup** with the source/publication
+approvals. If verification reports a mismatch, correct the files in GitHub
+and use **Check files again**. Current public docs stay live until activation.
+
+## 6. Verify the complete workflow
+
+Use a disposable repository for the first smoke test:
+
+1. Complete source setup and confirm the live docs in the public site.
+2. Save a doc edit in FrontLit, review the selected files, and submit a PR.
+ Confirm the PR was opened by your App and the public page is unchanged.
+3. Confirm **FrontLit / docs validation** appears in GitHub and inspect
+ FrontLit's private **Git checks** report.
+4. Merge the valid PR. Confirm a Push delivery succeeds in the App's
+ **Advanced → Recent Deliveries**, Source's live commit advances, and
+ **FrontLit / docs publication** reports the actual publication outcome.
+5. Confirm the public page reflects the merged text.
+
+An edit is a local draft until submitted. A PR opens a review; it does not
+publish. A merge triggers import and validation, then successful activation
+publishes. A merged commit can fail documentation validation and leave the
+last good publication live. FrontLit does not automatically merge PRs.
+
+For edited-document PRs, independent PR discovery runs on a fifteen-minute
+schedule. Setup-PR outcome tracking normally polls about once a minute.
+Webhooks provide the prompt path; missed-event reconciliation is a fallback.
+App Contents write permission does not enable direct commits in a workspace;
+that requires a separate explicit grant.
+
+## Troubleshooting
+
+| Symptom | What to check |
+| --- | --- |
+| Callback shows HTTP 502 | Confirm the API is healthy and the tunnel/proxy forwards to its actual port, default 4000. |
+| Installation unavailable or authorization lists no repositories | Check App visibility, the signed-in GitHub account, organization approval, installation repository selection and suspension. |
+| Setup expired or Authorize GitHub returns after a reload | Restart/reconnect the temporary setup authorization in the same FrontLit workspace; installing the App is not a permanent workspace setup credential. |
+| PR creation cannot write code | Grant Contents write and Pull requests write on the App, approve the installation upgrade, then review FrontLit's Git write grant. |
+| New PR has no validation check | Confirm Pull request subscription, Checks write, successful webhook deliveries and a healthy API worker. Push alone is insufficient. |
+| GitHub rerun does nothing | Confirm the Check run subscription and inspect its webhook delivery. |
+| Merge appears in GitHub but public docs are unchanged | Inspect Source's observed/validated/live commits and Git checks; an import may be pending or rejected. Refresh the setup PR status during initial setup. |
+| `DUPLICATE_ROUTE` | Give published docs unique routes. The report identifies conflicting files; correct Git and let the next push reconcile. Last good docs remain live. |
+| `UNSUPPORTED_NODE` on `.mdx` | Use safe Markdown. MDX components are not executed or silently imported. |
+
+Inspect delivery responses in the App's **Recent Deliveries** and redeliver
+failed events after fixing connectivity or secrets. A successful API health
+response alone does not prove callback, webhook or repository permissions.
+
diff --git a/frontlit/docs/self-hosting/index.md b/frontlit/docs/self-hosting/index.md
new file mode 100644
index 0000000..dd6ad60
--- /dev/null
+++ b/frontlit/docs/self-hosting/index.md
@@ -0,0 +1,189 @@
+---
+title: FrontLit Self Hosting Guide
+description: Run FrontLit, SendLit, and their dependencies from one Compose file.
+nav_order: 10
+---
+
+The root Compose file is the self-host stack. It runs:
+
+- PostgreSQL with separate `frontlit` and `sendlit` databases;
+- Redis for SendLit's delivery and automation workers;
+- the SendLit API at `http://localhost:3001`;
+- the FrontLit API at `http://localhost:4000`;
+- the FrontLit dashboard at `http://localhost:3000`; and
+- public sites at `http://localhost:3002`.
+
+FrontLit still talks to SendLit only over REST (`SENDLIT_API_URL`). Inside
+Compose that URL is the Docker network name `http://sendlit-api:3001`. Each
+FrontLit team is provisioned as a dedicated SendLit team.
+
+## Prerequisites
+
+Install Docker Compose and have a checkout of FrontLit.
+
+For a public deployment, place the published HTTP ports behind an HTTPS
+reverse proxy. Do not expose Postgres or Redis on a public interface.
+Postgres is published on loopback (`127.0.0.1:5433`) so a host process can
+still reach it; Redis stays on the Compose network.
+
+## Start the stack
+
+Create the Compose environment file:
+
+```bash
+cp .env.example .env
+```
+
+Generate independent secrets, for example with `openssl rand -base64 48`, and
+set these values in `.env`:
+
+```bash
+BETTER_AUTH_SECRET=
+PROVISIONING_SECRET=
+SUPER_ADMIN_EMAIL=admin@example.com
+
+SENDLIT_BETTER_AUTH_SECRET=
+SENDLIT_PIXEL_SIGNING_SECRET=
+SENDLIT_ESP_CREDENTIALS_ENCRYPTION_KEY=
+SENDLIT_SUPPRESSION_HASH_KEY=
+SENDLIT_SUPER_ADMIN_EMAIL=admin@example.com
+```
+
+`SUPER_ADMIN_EMAIL` creates the initial FrontLit administrator.
+`SENDLIT_SUPER_ADMIN_EMAIL` creates the initial SendLit administrator and
+organization.
+
+Start everything and confirm health:
+
+```bash
+docker compose up --build -d
+curl http://localhost:4000/health
+curl http://localhost:3001/health
+```
+
+The expected responses are JSON containing `"status":"ok"`. The first startup
+creates both databases and runs migrations. These actions are idempotent, so
+later `docker compose up -d` commands are safe.
+
+## Connect FrontLit to SendLit
+
+On its first boot, SendLit logs a one-time, scoped organization API key:
+
+```bash
+docker compose logs sendlit-api
+```
+
+Find the `organizationApiKey` value. It has the `sl_org_live_` prefix and is
+only shown when created. Set it in the root `.env`:
+
+```bash
+SENDLIT_API_URL=http://sendlit-api:3001
+SENDLIT_ORGANIZATION_API_KEY=sl_org_live_...
+```
+
+Then recreate the FrontLit API so it reads the new value:
+
+```bash
+docker compose up -d frontlit-api
+```
+
+New FrontLit teams will then be provisioned in the SendLit organization
+automatically. The organization key is used only to provision and manage
+those teams; FrontLit stores the separate, team-scoped SendLit key returned
+for each provisioned team.
+
+`SUPER_ADMIN_EMAIL` likewise prints a FrontLit team API key once:
+
+```bash
+docker compose logs frontlit-api
+```
+
+## Production configuration
+
+Use real domains and HTTPS before enabling production mode for SendLit:
+
+```bash
+SENDLIT_NODE_ENV=production
+SENDLIT_API_PUBLIC_URL=https://sendlit-api.example.com
+SENDLIT_WEB_CLIENT=https://sendlit.example.com
+SENDLIT_PROTOCOL=https
+SENDLIT_DOMAIN=sendlit.example.com
+```
+
+And point FrontLit at the public origins:
+
+```bash
+API_PUBLIC_URL=https://api.example.com
+WEB_CLIENT=https://app.example.com
+WEB_BASE_URL=https://app.example.com
+SITES_BASE_URL=https://sites.example.com
+PLATFORM_SITE_DOMAIN=example.com
+ENABLE_TRUST_PROXY=true
+```
+
+Better Auth rejects HTTP login URLs in production mode. Keep the root `.env`
+out of version control and use unique, high-entropy secrets for every
+deployment.
+
+## Local Bun development
+
+To connect documentation to GitHub, follow
+[Set up the documentation GitHub App](/docs/self-hosting/github-app). It covers
+App registration, repository permissions, event subscriptions, callbacks,
+webhooks and verification.
+
+Do not run `bun run dev:api`, `bun run dev:web`, or `bun run dev:sites` against
+this file — `frontlit-api` / `frontlit-web` / `frontlit-sites` bind the
+same ports. Use the local dependency stack instead:
+
+```bash
+docker compose -f docker-compose.local.yml up
+```
+
+Then start FrontLit from the checkout. See
+[Getting Started](/docs/getting-started/overview).
+
+## Operations
+
+```bash
+docker compose ps
+docker compose logs -f frontlit-api
+docker compose logs -f sendlit-api
+```
+
+The `frontlit-postgres-data` and `sendlit-redis-data` Docker volumes persist
+data across restarts. Back up the Postgres volume before upgrades or
+destructive maintenance. Do not run `docker compose down -v` unless you
+intend to delete all self-hosted FrontLit and SendLit data.
+
+## Troubleshooting
+
+### Email or Contacts says the team is not connected to SendLit
+
+The SendLit container being healthy is not enough. Each FrontLit team is
+connected only after FrontLit provisions a matching SendLit team and stores
+that team's API key. If `/email` or `/contacts` shows that the team is not
+connected:
+
+1. Confirm the root `.env` has the **current** `SENDLIT_ORGANIZATION_API_KEY`
+ from `docker compose logs sendlit-api`. SendLit prints this key only when
+ it first creates the super-admin organization. A later SendLit reset
+ issues a new key; the old one returns `401 unauthorized`.
+2. Recreate `frontlit-api` after changing that env var. The process reads
+ the environment at startup and will keep using a stale key until it
+ restarts.
+3. Reset any exhausted provisioning retries. Failed `provision_sendlit`
+ outbox jobs stop after eight attempts and are not picked up again:
+
+```sql
+UPDATE outbox_jobs
+SET status = 'pending',
+ attempts = 0,
+ last_error = NULL,
+ next_attempt_at = NOW()
+WHERE type = 'provision_sendlit'
+ AND status = 'failed';
+```
+
+The API worker polls every few seconds. When the job succeeds, `integrations`
+has a `sendlit` row for the team and the workspace pages show as connected.
diff --git a/frontlit/docs/teams/members.md b/frontlit/docs/teams/members.md
new file mode 100644
index 0000000..3ddbca1
--- /dev/null
+++ b/frontlit/docs/teams/members.md
@@ -0,0 +1,112 @@
+---
+title: Team members and permissions
+description: Invite teammates and give each person only the access they need.
+nav_order: 10
+---
+
+Each FrontLit team has one **Owner** and any number of members. The owner can
+use every team permission. Every other member receives an explicit set of
+permissions; FrontLit does not use stored roles such as Admin or Editor.
+
+The same person can have different access in different teams.
+
+## Create or join your first team
+
+Signing in does not automatically create a team. If you are new to FrontLit,
+the Account screen lets you create a workspace explicitly. If someone invited
+you, open the link from the invitation email instead. Accepting it adds you to
+that team and opens the first workspace area you are allowed to use; it does
+not create a separate personal team.
+
+## Invite a teammate
+
+1. Select the team in the workspace switcher.
+2. Open **Settings → Members**.
+3. Choose **Invite**, enter the person's email address, then choose a preset or
+ a custom permission set.
+4. Send the invitation. The recipient must sign in with that same email
+ address before they can review and accept it.
+
+Invitations expire after seven days. You can resend or revoke a pending
+invitation from the Members screen. Resending creates a new link and makes the
+previous one unusable.
+
+## Choose permissions
+
+Presets are shortcuts, not roles. Choosing one copies its permissions into the
+member's access; you can then use **Custom** when a different combination is
+needed.
+
+| Preset | Intended access |
+| --------------- | ------------------------------------------------------------------------------------------------------------------- |
+| Full access | All non-owner team permissions. It does not grant ownership or API-key management. |
+| Content manager | Pages, themes, media, articles, documentation, and product updates. |
+| Support | Inbox/help desk and documentation read access. |
+| Marketing | Contacts, segments, and broadcast drafting. Sending a broadcast requires the separate `broadcasts:send` permission. |
+| Read only | Read access across available product areas. |
+| Custom | Select the exact permissions to grant. |
+
+Write permissions include the matching read permission. For example,
+`pages:write` also lets a member view pages. Some permissions are marked as
+sensitive in the picker because they can affect members, domains, AI settings,
+general settings, or live broadcasts.
+
+## What members see
+
+FrontLit hides workspace areas a member cannot access. If someone follows an
+old bookmark or switches to a team where their current page is unavailable,
+FrontLit takes them to the first area they can access instead. This keeps the
+workspace focused, but permissions are always enforced by the API as well.
+
+A member may be able to view a screen without having permission to change it.
+Controls they cannot use are disabled or omitted. If permissions change, the
+next request uses the new access immediately.
+
+For content libraries, read access lets a member browse existing items. Opening
+an editor or creating, changing, publishing, or deleting content requires that
+area's write permission. Direct editor links without write access return the
+member to the readable library instead of showing an application error.
+
+Articles, documentation, and product updates appear under one **Content**
+heading in the permission picker, but retain separate read/write pairs. This
+lets a documentation writer work without also gaining authority to publish
+articles or import product updates.
+
+## Manage members safely
+
+Members with **Members: invite** can invite people only with permissions they
+already hold. Members with **Members: manage** can edit or remove only
+non-owners whose access is no broader than their own. This prevents someone
+from granting or changing authority above their level.
+
+The owner can edit or remove any non-owner. A member can leave a team at any
+time. The owner must transfer ownership to an existing member before leaving
+or deleting the team. Ownership transfer and team deletion require a fresh
+email-code verification.
+
+## Owners and API keys
+
+The team owner is identified by the team itself, not by a permission preset.
+Owners cannot be edited or removed through the member editor. Transfer
+ownership when responsibility changes.
+
+Team API keys currently have full-team access, so only the owner can create,
+list, or revoke them. Treat every API key as a sensitive integration secret.
+
+## OAuth scopes are separate
+
+Human permissions control what a signed-in person may do in a team. OAuth
+scopes control what an external client may do for that person. For an OAuth
+request to succeed, **both** the current team permission and the token's scope
+must allow the action. Changing team permissions takes effect immediately,
+even for a token that still has a matching scope.
+
+An OAuth connection belongs to a person's account and works in every team they
+are a member of. When someone joins your team, the apps they've already
+connected can act in it within their permissions; when they leave or are
+removed, those apps lose access to your team on their next request. Changes a
+connected app makes to members or API keys are recorded with its OAuth client
+ID.
+
+See [Authentication](/docs/developers/authentication) for API keys, browser team
+selection, and OAuth authentication details.