Scratch is two executable programs that can be used together to create and publish static websites with Markdown and React:
The Scratch CLI is a command line utility for scaffolding, building, and publishing Scratch projects as static websites.
The Scratch server is a server for serving static websites built with the Scratch CLI. There's a public server hosted on scratch.dev and you can host your own on Cloudflare.
Install:
curl -fsSL https://scratch.dev/install.sh | bashCreate a project and start the dev server:
scratch create my-site
cd my-site
scratch devWrite your content in pages/index.mdx, then publish:
scratch publishThat's it. Your site is live.
Scratch uses a simple project structure to avoid the need for boilerplate and configuration:
Markdown files in pages/ become pages on your site:
| File | URL |
|---|---|
pages/index.mdx | / |
pages/about.mdx | /about/ |
pages/posts/hello.mdx | /posts/hello/ |
Use .md for plain Markdown or .mdx to include React components.
Scratch uses mdx to translate markdown elements into React components. You can customize the default components used in this translation by adding new ones to src/markdown/
Components in pages/ or src/ are automatically available in MDX files. A Counter.tsx file lets you write <Counter /> in any page without importing it.
# My Page
Here's a counter: <Counter />PageWrapper componentThe page wrapper component (your-project/src/template/PageWrapper.tsx) is used in Scratch's html template to wrap your pages. It is the only component that the build process expects to exist.
You can modify this component to change the look and feel of your site.
Put static content like images in the public/ directory. These files will be copied directly to your built site.
Static files can also live in pages/ next to your content. Scratch will automatically translate relative file system paths like this...
...into their corresponding URL path equivalents.
Scratch uses Tailwind CSS. Edit src/tailwind.css to change the styling of your pages:
Markdown components are styled with Tailwind Typography.
<div className="prose prose-lg prose-slate">
{children}
</div>Add YAML frontmatter for page metadata:
---
title: "My Page"
description: "Brief description for SEO"
author: "Your Name"
image: "/og-image.png"
---Common properties: title, description, keywords, author, image, robots, lang, canonical.
For social sharing: url, type, siteName, locale, twitterCard, twitterSite.
For articles: publishDate, modifiedDate, tags.
The CLI builds your project and publishes it to a Scratch server. Install it with:
curl -fsSL https://scratch.dev/install.sh | bashor
curl -fsSL https://scratch.dev/install.md | claudeUpdate to the latest version:
scratch updateAll commands support these flags:
| Flag | Description |
|---|---|
-v, --verbose | Show detailed output |
-q, --quiet | Errors only |
--show-bun-errors | Full Bun stack traces |
--version | Show version |
--help | Show help |
Create a new project:
scratch create my-siteStart a development server with hot reload:
scratch devOptions:
-p, --port <port> — Server port (default: 5173)-n, --no-open — Don't open browser-d, --development — Unminified output with source maps-b, --base <path> — Base path for deployment--highlight <mode> — off, popular, auto (default), or allBuild for production:
scratch buildOutput goes to dist/. Options are the same as scratch dev, plus:
-o, --out-dir <path> — Output directory (default: dist)--no-ssg — Skip static site generationFor more on how the build works, see Build Pipeline.
Preview your production build locally:
scratch previewRuns a local server for dist/. Build first with scratch build.
Remove build artifacts:
scratch cleanDeletes dist/, .scratch/cache/, and .scratch/dev/.
Reset a template file to its default:
scratch eject --list # See available files
scratch eject src/markdown/CodeBlock.tsxBuild and deploy your project:
scratch publishOn first publish, you'll be asked for:
Options:
--server <url> — Server URL--name <name> — Project name--visibility <v> — Access control--no-build — Deploy existing dist/--dry-run — Show what would be deployedProject names must be 3-63 characters, lowercase letters/numbers/hyphens, starting with a letter. Reserved names: api, auth, admin, www, app, help, support, static, assets, cdn, files, upload, download.
For more on servers and URLs, see Scratch Server.
Log in to a Scratch server:
scratch login # Default: app.scratch.dev
scratch login https://my-server.comThe CLI shows a verification code and opens your browser. Sign in and approve the device. Credentials are saved to ~/.scratch/credentials.json.
You can be logged in to multiple servers at once. Each server has its own entry in the credentials file.
Options:
--timeout <minutes> — How long to wait for approval (default: 10)Log out from a server:
scratch logout
scratch logout https://my-server.comConfigure project settings interactively:
scratch configUpdates .scratch/project.toml with server URL, project name, and visibility.
Check who you're logged in as:
scratch whoamiManage your projects on a server.
List projects:
scratch projects # or: scratch projects lsGet project info:
scratch projects info my-blogDelete a project:
scratch projects rm my-blog
scratch projects rm my-blog -f # Skip confirmationCreate time-limited links for sharing private projects without requiring login.
Create a share link:
scratch share my-blog --name "Client review" --duration 1wDurations: 1d (1 day), 1w (1 week), 1m (1 month).
List share tokens:
scratch share ls my-blogRevoke a token:
scratch share revoke tok_abc123 my-blogConfigure Cloudflare Access credentials for servers that use it:
scratch cf-access https://my-server.comPrompts for Client ID and Client Secret from your Cloudflare Access service token. Only needed for self-hosted servers using Cloudflare Access.
Create and manage API tokens for CI/CD and automation. Unlike session tokens from scratch login, API tokens don't require browser interaction.
Create a token:
scratch tokens create my-ci-token
scratch tokens create deploy-key --expires 90 # Expires in 90 daysToken names must be 3-40 characters using letters, numbers, hyphens, and underscores.
List tokens:
scratch tokens lsRevoke a token:
scratch tokens revoke my-ci-token
scratch tokens revoke <token-id>Store a token for CLI use:
scratch tokens use scratch_abc123...Using API tokens:
# Option 1: Environment variable (recommended for CI/CD)
export SCRATCH_TOKEN=scratch_...
scratch publish
# Option 2: Project .env file
echo "SCRATCH_TOKEN=scratch_..." >> .env
scratch publish
# Option 3: Store in credentials file
scratch tokens use scratch_...
scratch publishToken resolution priority: SCRATCH_TOKEN env var → .env file → ~/.scratch/credentials.json
With Cloudflare Access: API tokens work with servers behind Cloudflare Access. First configure CF Access credentials with scratch cf-access, then use your API token normally. The CLI sends both the CF Access service token (to pass the network layer) and your API token (to authenticate).
When you run scratch build, here's what happens:
package.jsonpublic/dist/Remark plugins (Markdown processing):
not-proseRehype plugins (HTML processing):
Syntax highlighting uses Shiki. Control with --highlight:
scratch build --highlight popular # Common languages only
scratch build --highlight all # All languages
scratch build --highlight off # DisableBuild cache lives in .scratch/cache/. Clear it with scratch clean.
Quick preview for a single file or directory. Useful for viewing README files:
scratch watch README.md
scratch watch ./notes/Options:
-p, --port <port> — Server port (default: 5173)-n, --no-open — Don't open browserA Scratch server lets you share your work with others. Publish privately to share with specific people or your team, or make it public for anyone to see.
The server runs on Cloudflare (Workers, D1, and R2). You can use the hosted version on scratch.dev or run your own.
We host a public Scratch server instance on scratch.dev Anyone can publish public or private projects for free while Scratch is in Preview.
Note: Projects on scratch.dev expire after 30 days during the Preview period.
When you publish, your site is available at either of the following:
https://pages.scratch.dev/<your-email>/<project-name>/
https://pages.scratch.dev/<your-user-id>/<project-name>/
For example, if you're pete@example.com and your project is my-blog:
https://pages.scratch.dev/pete@example.com/my-blog/
https://pages.scratch.dev/abc123/my-blog/
Self-hosted Scratch servers that are restricted to accounts on @example.com support simpler routing:
https://pages.mydomain.com/pete/my-blog/
Preview warning: Scratch is in preview. Don't use it for anything sensitive or critical. The code is open source—if you need more security, self-host behind Cloudflare Access.
Control who can access your site:
| Mode | Description |
|---|---|
public | Anyone |
private | Only you |
@domain.com | Anyone with that email domain |
user@example.com | Specific person |
a@x.com,b@y.com | Multiple people |
Set visibility when publishing:
scratch publish --visibility private
scratch publish --visibility @mycompany.com
scratch publish --visibility "alice@x.com,bob@y.com"Or in .scratch/project.toml:
[project]
name = "my-blog"
visibility = "private"When someone without an access token visits your project, they'll be prompted to sign in. If they authenticate and don't have access, they'll see a 404 error.
Run your own Scratch server when you need:
Self-hosting is also useful for a personal website or a shared space where colleagues can share writing privately.
A Scratch server needs:
git clone https://github.com/scratch/scratch
cd scratchbun ops server -i <server-name> setupAuthentication modes:
BetterAuth (default) — Users sign in with Google OAuth to authenticate their CLI and view private pages.
AUTH_MODE=local
GOOGLE_CLIENT_ID=<from-google-console>
GOOGLE_CLIENT_SECRET=<from-google-console>Cloudflare Access — Enterprise SSO. Supports SAML, OIDC, Okta, Azure AD. Good for corporate deployments.
AUTH_MODE=cloudflare-access
CF_ACCESS_TEAM_NAME=your-team
CF_ACCESS_AUD=<application-aud>Domain configuration:
Scratch uses two subdomains:
app.example.com) — API, login, dashboardpages.example.com) — Published sitesThis separation prevents malicious uploaded JavaScript from stealing app session cookies.
# Run any needed migrations on your Scratch Server database
bun ops server -i <server-name> db migrate
# Deploy the scratch server
bun ops server -i <server-name> deployYou can configure your Scratch server to serve a specific project on the root domain and www subdomain:
WWW_PROJECT_ID in server/.<your-server-name>.vars with this value.bun ops server -i <server-name> config push to update your server's Cloudflare Secrets configScratch separates the app and content subdomains for security. The app handles authentication and API requests. Content serves user-uploaded files.
This is important because published sites can include JavaScript. If session cookies were shared across subdomains, a malicious site could make authenticated API requests. App session cookies are scoped to the app subdomain so that client-side JavaScript code on pages. can't access it.
When a user attempts to load content on pages., the server first checks whether the URL maps to a specific project.
If it does, and the project has visibility=public, the content will be served right away.
If it does and the project has a restricted visibility setting, the user will be redirected to app. to authenticate. If they have access to the project in question they'll be redirected back to pages. with a content access token. This token is stored in an http-only cookie scoped to the project's base URL path. It is only valid for that project.
If the URL does not map to a project, the user will be redirected to authenticate and will be served a 404 error if they do.
Preview warning: Scratch is new and hasn't been audited. Don't rely on local authentication for sensitive content. If you need stronger security, self-host behind Cloudflare Access.
The server has a REST API. Authenticate with a Bearer token:
curl -H "Authorization: Bearer <token>" https://app.scratch.dev/api/meGet your token from ~/.scratch/credentials.json after running scratch login.
GET /api/me — Current user
{
"user": {
"id": "abc123",
"email": "you@example.com",
"name": "Your Name"
}
}GET /api/projects — List projects
GET /api/projects/:name — Get project
POST /api/projects — Create project
curl -X POST -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "my-project"}' \
https://app.scratch.dev/api/projectsPATCH /api/projects/:name — Update project
curl -X PATCH -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"visibility": "private"}' \
https://app.scratch.dev/api/projects/my-blogDELETE /api/projects/:name — Delete project
POST /api/projects/:name/deploy — Deploy
curl -X POST -H "Authorization: Bearer <token>" \
-H "Content-Type: application/zip" \
--data-binary @dist.zip \
"https://app.scratch.dev/api/projects/my-blog/deploy?visibility=public"GET /api/projects/:name/share-tokens — List share tokens
POST /api/projects/:name/share-tokens — Create share token
curl -X POST -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "Client review", "duration": "1w"}' \
https://app.scratch.dev/api/projects/my-blog/share-tokensDELETE /api/projects/:name/share-tokens/:id — Revoke share token
| Code | Meaning |
|---|---|
PROJECT_NAME_INVALID | Bad project name format |
PROJECT_NAME_TAKEN | You already have this project name |
PROJECT_NOT_FOUND | Project doesn't exist or you don't own it |
VISIBILITY_INVALID | Bad visibility format |
VISIBILITY_EXCEEDS_MAX | Visibility exceeds server maximum |
INVALID_ZIP | Not a valid zip file |
DEPLOY_TOO_LARGE | Zip too big |
TOO_MANY_FILES | Too many files in deploy |
SHARE_TOKEN_NOT_FOUND | Token doesn't exist |
SHARE_TOKEN_LIMIT_EXCEEDED | Too many active tokens |
If you run into an issue with Scratch, point your favorite coding agent at our repo and ask it to figure out what's going on. Feel free to open an issue with a markdown plan for fixing the problem. If your plan makes sense we'll implement it.
Send feedback and requests to @koomen