---
name: plurabase-deploy-site
description: Package a static frontend so Plurabase Sites accepts it and connects it to its project backend; a person then uploads and publishes the ZIP in the private-pilot console.
---

# Package a frontend for Plurabase Sites

> Status: Available in pilot (sites.zip)

> Status: In development (sites.git)

Use this when a project's web frontend should be served by Plurabase Sites. Sites serves prebuilt static files only. Git deploys are in development, so the output of this skill is a ZIP that a person uploads in the console. Facts: https://plurabase.com/docs/facts/

## Steps

1. Read the backend from the runtime file instead of hardcoding it. Keep the code inside a function: the only checked build is an esbuild IIFE bundle, which rejects top-level `await`. Call `connect()` once at start-up and keep the client.

```ts
import { createClient } from '@supabase/supabase-js'

export async function connect() {
  const reply = await fetch('./__simplybase/config.json', { credentials: 'omit', cache: 'no-store', redirect: 'error' })
  if (!reply.ok) throw new Error('Project connection unavailable')
  const config = await reply.json()
  return createClient(config.api_url, config.publishable_key, {
    auth: { persistSession: false, detectSessionInUrl: false },
    global: { fetch: (input, init) => fetch(input, { ...init, credentials: 'omit' }) },
  })
}
```

2. Build a single-page static app. Each release is served under `/__sites/r/<release>/`, so every URL to the app's own files must be relative (`./assets/app.js`); a root-relative `/assets/app.js` fails. The Content Security Policy blocks inline `<script>` and `<style>`, `style="…"` attributes, inline event handlers, `eval`, `new Function`, `data:` fonts and third-party CDNs, so bundle every dependency, including `@supabase/supabase-js`, into files. It also blocks `<style>` elements added at runtime, so do not use CSS-in-JS libraries that inject them, such as emotion or styled-components; ship plain CSS files.
3. With Vite, set `base: './'` and `build.assetsInlineLimit: 0`, and do not use `@vitejs/plugin-legacy`. Tell the user that no Vite build has run on Sites yet; the only checked build is a classic esbuild bundle referenced as `./assets/…`.
4. Do not use Next.js with `output: 'export'` or Astro: their output needs inline scripts and root-relative `/_next/` or `/_astro/` URLs, which Sites blocks.
5. Use hash-based routing or a single page: there is no SPA fallback, so deep links to client routes return 404.
6. Zip the contents of the output directory so `index.html` is at the archive root. Use ASCII file names. Keep the ZIP under 4 MiB and the expanded files under 16 MiB.
7. Hand the ZIP to the user to upload in the console's Sites page and publish with **Publicar**.

## Guardrails

- Never put service-role keys, database passwords or MCP tokens in frontend files.
- Each Site accepts two uploads in its lifetime, including failed ones. Check the archive before handing it over instead of uploading to test.
- The page runs in a sandbox without form submission: a `<form>` never fires `submit`. Start actions from a button's `click` and the Enter key.
- The sandbox also blocks `alert`, `confirm` and `prompt`: `confirm` returns `false` at once, so a guard such as `if (confirm('Delete?'))` never runs its action. Build confirmations as a dialog inside the page. Popups and new-tab links (`window.open`, `target="_blank"`) and downloads (`<a download>`) are blocked too.
- Browser requests (`fetch`, XHR, WebSocket) reach only the release and the project API. Do not call third-party APIs from the browser. Iframes, audio and video, and web workers are blocked.
- Site code runs on an opaque origin: no cookies, `localStorage` or service workers, so users sign in again after a reload.
- No server-side rendering, Node.js server, middleware or custom headers.

## Verify

- `unzip -l site.zip` lists `index.html` without a leading folder.
- The archive is under 4 MiB and contains no `.env`, key or token files.
- In the output directory (here `dist`), each of these prints nothing; the last one skips JSON data blocks such as `<script type="application/json">`, which are allowed:

```sh
grep -rnoE '(src|href)=[^.a-zA-Z0-9]?/' dist --include='*.html'
grep -rnoE 'url\([^.a-zA-Z0-9]?/' dist --include='*.css'
grep -rnoE '<style|style=' dist --include='*.html'
grep -rnoE '<script[^>]*>' dist --include='*.html' | grep -v 'src=' | grep -vE 'type="application/(ld\+)?json"'
```
