---
name: ployin
description: "Deploy static websites to Ployin (https://ployin.dev) with the `ployin` CLI and give them a backend (auth, data, file storage, realtime) with @ployin/sdk. Use when the user wants to publish, host, deploy or share a website or web app, put an HTML page online, get a public link for a build, roll back or take down a Ployin site, or add sign-in, a database, uploads or live updates to a site hosted on Ployin."
---

# Ployin

Ployin hosts static sites. A folder (or a build output such as `dist/`) is
uploaded as a zip and served at **`https://<project>.ployin.dev`**
(`ployin.xyz` addresses currently redirect there). Every deploy is a new
version; rollback is one command. Each project also gets its own backend
(its own database, users and files) that browser code reaches with
`@ployin/sdk`.

- Dashboard: https://ployin.dev (sign up, upload a zip, see versions)
- This guide: https://ployin.dev/skill.md · Overview for AI: https://ployin.dev/llms.txt

## Usage

```
/ployin                 deploy this project (build first if needed)
/ployin deploy <dir>    deploy a specific directory
/ployin status          list projects / history
/ployin rollback <p>    roll a project back
/ployin backend         add auth, data, storage or realtime with @ployin/sdk
```

With no argument, work out what the user wants from the conversation; most
often it is "deploy this".

---

## 1. Make sure the CLI is installed

```bash
ployin --version
```

If that fails, install it (Linux, macOS; puts `ployin` in `~/.local/bin`):

```bash
curl -fsSL https://ployin.dev/install.sh | sh
```

- Afterwards, if `ployin` is still not found, use `~/.local/bin/ployin` or add
  `~/.local/bin` to `PATH`.
- The installer (and the first `ployin` command) also keeps this skill up to
  date for Claude Code (`~/.claude/skills/ployin/`), Codex and Gemini CLI
  (`~/.agents/skills/ployin/`) and, when present, Copilot.
- Already installed but old? `ployin update` (check first with
  `ployin update --check --json`).
- Windows: download `https://get.ployin.dev/v0.1.2/ployin-windows-x86_64.exe`,
  rename it to `ployin.exe` and run it from there.
- With a Rust toolchain and the repository: `cargo install --path cli`.
- Without any of these, the user can upload a zip in the dashboard instead.

## 2. Make sure you are logged in

```bash
ployin whoami --json
# {"loggedIn": true, "email": "...", "role": "...", "apiUrl": "https://ployin.dev"}
```

If `loggedIn` is false:

- **Ask the user** for their Ployin email and password, or for a token. Never
  invent credentials, never write them into files, never print them back.
- Log in without a prompt:
  ```bash
  PLOYIN_PASSWORD='…' ployin login -e user@example.com --json
  ```
- No account yet: ask before creating one, then
  `PLOYIN_PASSWORD='…' ployin signup -e user@example.com --json`
  (password at least 8 characters).
- In CI or a sandbox, a token works without saving anything:
  `export PLOYIN_TOKEN=…` (the user gets one with `ployin token` on their machine).
- Other servers: add `--api-url https://their-ployin.example` (saved with the login).

## 3. Build, then deploy

1. **Find the output directory.** Look at `package.json` scripts and the
   framework config:

   | Stack | Build | Deploy this directory |
   |---|---|---|
   | Plain HTML/CSS/JS | none | the folder with `index.html` |
   | Vite (React, Vue, Svelte, Solid, Preact…) | `npm run build` | `dist` |
   | Create React App | `npm run build` | `build` |
   | Next.js | `output: 'export'` in next.config, `npm run build` | `out` |
   | Nuxt | `npx nuxi generate` | `.output/public` |
   | SvelteKit | `adapter-static`, `npm run build` | `build` |
   | Astro | `npm run build` | `dist` |
   | Angular | `npm run build` | `dist/<app>/browser` |
   | Hugo | `hugo` | `public` |
   | Jekyll | `jekyll build` | `_site` |
   | Eleventy | `npx @11ty/eleventy` | `_site` |

   Use the package manager the lockfile shows (npm, pnpm, yarn, bun).
   Ployin serves files only: no server-side rendering, no API routes, no
   build step on the server. Environment variables must be baked in at build
   time (`VITE_…`, `NEXT_PUBLIC_…`).

2. **Single-page apps** (client-side routing): make sure unknown paths load
   the app by adding a `200.html` that is a copy of `index.html`:
   ```bash
   cp dist/index.html dist/200.html
   ```
   A `404.html` is served (with status 404) for missing pages when there is no
   `200.html`.

3. **Pick a project name**: 3–63 characters, lowercase letters, digits and
   hyphens, not starting or ending with a hyphen. It becomes the subdomain.
   Names are global: if one is taken by another account, choose another
   (ask the user). Reserved: `api www mail admin status ftp`, and some others.

4. **Check what will be uploaded** (optional, quick):
   ```bash
   ployin deploy dist --project my-site --dry-run --json
   ```
   Left out automatically: `node_modules/`, `.git/`, dot-files (except
   `.well-known/`). A `.ployinignore` file adds glob patterns, one per line.

5. **Deploy**:
   ```bash
   ployin deploy dist --project my-site --json
   # {"project":"my-site","version":3,"files":42,"url":"https://my-site.ployin.dev","hasSourceCode":false}
   ```
   Deploying the *project root* that contains a `dist/` folder also works:
   `dist/` is served and the rest is kept as downloadable source.

6. **Verify and report**: `curl -sI <url>` should answer 200. Give the user the
   URL from the JSON output. Pages are cached at Cloudflare for up to 30
   seconds, so right after a redeploy (or rollback) the old version can still
   show; to check the new one at once, add a throwaway query string
   (`<url>?v=123`). A brand-new project can answer 404 for up to 10 seconds.

## 4. Manage sites

```bash
ployin list --json                          # projects: name, version, fileCount, deployedAt, url
ployin history my-site --json               # versions, newest first, with isActive
ployin rollback my-site --json              # back to the version before the live one
ployin rollback my-site --version 2 --json  # to a specific version
ployin download my-site --out ./backup --json            # live web files as a zip
ployin download my-site --source --version 3 --json      # source code of v3
ployin teardown my-site --yes --json        # DELETE the project and all versions
ployin domains my-site --json               # custom domains and the DNS records they need
ployin domains my-site add www.example.com --json     # then add the TXT + CNAME records it returns
ployin domains my-site verify www.example.com --json  # checks DNS; HTTPS follows within minutes
ployin keys my-site --json                  # the project's backend URL, anon key and service key
ployin update --json                        # update the CLI itself (verifies SHA-256)
```

- **Teardown is permanent.** Only run it when the user clearly asked, and
  say which project first. Without `--yes` it refuses when there is no terminal.
- Rollback is safe and reversible (every version is kept, up to 30).

## 5. Reading results and errors

With `--json`, success prints one JSON document on stdout; failure prints
`{"error": "...", "status": <http status or null>}` on stderr and exits 1.
Progress messages go to stderr.

| Error | Meaning / what to do |
|---|---|
| `Not logged in…` | Step 2. |
| status 401 | Token expired or invalid: log in again. |
| status 409 `…already taken by another account` | Pick another project name. |
| status 409 `…identical to the current active deployment` | Nothing changed since the last deploy; the site is already up to date. Not a failure for the user. |
| status 409 `A deploy … is already in progress` | Wait a few seconds and retry. |
| status 400 `Project name must be…` / `is reserved` | Fix the name. |
| status 413 | Upload over ~95 MB, or the zip unpacks too large: deploy only the build output; exclude big files with `.ployinignore`. |
| `Nothing to deploy` | The directory is empty after excludes; check the build output path. |
| `Could not reach …` | Network or wrong `--api-url`. |

---

## 6. Add a backend with @ployin/sdk

For sign-in, saving data, uploads or live updates from the browser.

```bash
npm install https://ployin.dev/sdk/ployin-sdk-0.2.0.tgz   # or pnpm add / yarn add / bun add the same URL
```

`@ployin/sdk` is served by Ployin, not by the npm registry (`npm install
@ployin/sdk` fails). Newest version: https://ployin.dev/sdk/latest.json.
Plain HTML without a build step can import it directly:
`import { createClient } from 'https://ployin.dev/sdk/0.2.0/index.js'`.

```ts
// src/lib/ployin.ts
import { createClient } from '@ployin/sdk'
export const db = createClient(import.meta.env.VITE_PLOYIN_URL, import.meta.env.VITE_PLOYIN_ANON_KEY)
```

```bash
# .env (public values, baked in at build time)
VITE_PLOYIN_URL=https://<project>.ployin.dev
VITE_PLOYIN_ANON_KEY=<the project's anon key>
```

Get the URL and keys with `ployin keys <project> --json` (the project must
be deployed once; logged in as its owner; CLI 0.1.2 or newer, run
`ployin update` if `keys` is unknown), or from the project's page in the
dashboard (**Backend**). Each project's backend is separate: a new project
starts empty. **Never** put the service key in browser code: it bypasses all
access rules. If a key leaked: `ployin keys <project> --rotate`.

```ts
// Auth
await db.auth.signUp({ email, password })
await db.auth.signInWithPassword({ email, password })
const { data } = await db.auth.getUser()
await db.auth.signOut()

// Collections (JSON documents). When signed in, db.from('notes') is private to
// that user (stored as pu_<userId>_notes).
const { data, error, count } = await db.from('notes').eq('done', false).order('createdAt', { ascending: false }).limit(20).select('*')
await db.from('notes').insert({ text: 'Hello' })
await db.from('notes').eq('id', id).update({ done: true })
await db.from('notes').eq('id', id).delete()
// filters: eq neq gt gte lt lte like ('%word%') in is(null)

// Storage
await db.storage.from('avatars').upload('me.png', file)
const url = db.storage.from('avatars').getPublicUrl('me.png')   // public buckets

// Realtime
db.channel('notes').on('INSERT', (e) => console.log(e.new)).subscribe()
```

Every call returns `{ data, error }`; check `error` (`error.message`,
`error.status`). Buckets are created with the project's service key (or in
the dashboard: Database → Storage). Access rules (row-level security
policies) are set with the service key too. A collection without policies
is **private**: only the service key reaches it,
except a signed-in user's own collections (`db.from('notes')` after sign-in
becomes `pu_<user id>_notes`, which only that user can use). For shared data,
such as public posts or a guestbook, add policies (with the service key, from
a terminal or script, never the browser), e.g.
`{"collection":"posts","operation":"SELECT","definition":{}}` (anyone may read) or
`{"collection":"todos","operation":"INSERT","definition":{"owner":"$auth.uid()"}}`
(see https://ployin.dev/docs → Database). If reads come back empty or writes
fail with `rls_violation`, a policy is missing; add it or tell the user.

```bash
curl -X POST https://ployin.dev/rls/v1/policy \
  -H "apikey: $SERVICE_KEY" -H "Authorization: Bearer $SERVICE_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"collection":"posts","operation":"SELECT","definition":{}}'
```

Limits per project: 100 MB of database and 1 GB of files (writes past them
answer `507`).

---

## Rules of thumb

- Build locally, deploy only the output folder, report the URL.
- Ask before: creating an account, choosing a name for the user, teardown.
- Keep secrets out of the repository and out of chat output.
- Prefer `--json` and parse it; do not scrape the human-readable output.
