Website generation contract

# Short Korea: website generation contract

Contract version: 1.0.0
SDK version: 1.0.0
Canonical manual: https://xn--zs5bn2c.xn--3e0b707e/llms-full.txt
HTTP schema: https://xn--zs5bn2c.xn--3e0b707e/openapi.json

## Instructions for coding agents

Apply this contract whenever a user asks you to create a website for 짧은.한국. Infer persistence, file uploads and access policy from the requested behavior. The user can describe the website in ordinary language; configure its storage and permissions as part of your implementation.

1. Deliver exactly one complete HTML file, including its CSS and JavaScript. Provide the finished HTML as a downloadable file. Use the user's language for the website interface.
2. Implement saved application data with window.short.db and user file uploads with window.short.storage. Data that should survive refresh or reopening belongs in these APIs.
3. The platform injects window.short before application scripts and binds it to the published project. Initialize the application after await short.ready. Project IDs, sessions and storage credentials are supplied by the host.
4. Declare shared collections and shared files with the inline short-config JSON shown below. Undeclared collections and files use visitor access.
5. Include loading, empty and error states. Disable duplicate submissions while saving. Preserve user input when a request fails and show the error message with a useful next action.
6. Render user content with textContent or safe DOM operations. Use actual saved data for counts, dates and results.
7. Verify saving, refreshing, reopening and any file upload in the published website. The SDK becomes available when the HTML is served through its short address.

## Artifact and runtime

The artifact is one HTML file up to 2 MiB (2,097,152 bytes). Put styles in <style> and scripts in <script>. The original filename may be any .html or .htm name; the platform publishes it as index.html. Multiple screens use JavaScript or hash routing within the same file. Default images can be inline SVG, data URLs or public HTTPS resources. Visitor uploads use short.storage.

The HTML runs in an iframe with sandbox="allow-scripts allow-forms allow-downloads". HTTPS scripts, styles, fonts, images and CORS-enabled network services are available. Use the short SDK for persistent data and files. The runtime has an opaque origin: implement application dialogs in the DOM, and keep screens in this HTML. Nested frames, popups, top-level navigation, camera, microphone and geolocation are restricted by the host policy. External services that require a normal same-origin cookie session need a different design.

The short SDK communicates with its parent host through a project-bound bridge. Owner credentials and visitor session tokens stay in the host. Keep deployment tokens and Firebase owner credentials in the deployment tool's environment, outside the visitor HTML.

## Access declaration

Place exactly one configuration script inside the HTML. Its JSON is limited to 32 KiB. It is read when a version is published or restored.

```html
<script type="application/json" id="short-config">
{
  "version": 1,
  "collections": {
    "posts": { "access": "public" },
    "responses": { "access": "submit" },
    "todos": { "access": "visitor" },
    "settings": { "access": "private" }
  },
  "files": { "access": "public" },
  "spa": false
}
</script>
```

| Collection access | Visitor capabilities | Owner capabilities |
| --- | --- | --- |
| visitor | Read and create their own records; update and delete their own records | Manage all records in My Sites |
| public | Read all records; create, update and delete their own records | Manage all records in My Sites |
| submit | Read, create, update and delete their own submissions | Review and manage all submissions in My Sites |
| private | Owner-only collection | Read and edit in My Sites |

Collection names match ^[A-Za-z][A-Za-z0-9_-]{0,47}$. Each record contains a JSON object. Undeclared collections default to visitor access. Visitor identity is maintained per project in the host browser. Clearing browser storage or using another browser creates a different visitor identity. Owner management is performed through the authenticated My Sites interface.

File access accepts visitor, public or private. Public files can be viewed by all visitors while the project is active. Visitor files are visible to their uploader. Private files are managed by the owner. Only the uploader or owner can delete a file. A shared photo board therefore declares both its collection and files as public.

## Database SDK

```javascript
await short.ready;
const posts = short.db.collection('posts');

const record = await posts.add({ title: 'First post', body: 'Hello' });
const saved = await posts.get(record.id);
console.log(saved.data.title);

const page = await posts.list({ limit: 20 });
for (const item of page.records) console.log(item.id, item.data);
if (page.nextCursor) {
  const next = await posts.list({ limit: 20, after: page.nextCursor });
}

// update replaces the complete data object. Preserve fields you still need.
await posts.update(record.id, { title: 'Edited post', body: 'Updated text' });
await posts.remove(record.id);
```

Records return {id, data, createdAt, updatedAt}; timestamps are ISO 8601 strings. Lists return {records, nextCursor}, newest first. Request 1 to 50 records per page, default 20. Pass nextCursor as after to load another page. The list quota is charged by the requested limit: list({limit:20}) reserves 20 reads even if fewer records exist. get costs one read; add, update and remove each cost one write.

A data object is limited to 16 KiB of UTF-8 JSON, 10 levels of nesting and 1,000 fields in total. Arrays, nested arrays, strings, booleans, null and finite numbers are supported. Keep binary content in file storage and store file IDs in records.

## File SDK

```javascript
await short.ready;
const selected = document.querySelector('input[type=file]').files[0];
const file = await short.storage.upload(selected);

try {
  await short.db.collection('posts').add({ title: 'Photo', fileId: file.id });
} catch (error) {
  await short.storage.remove(file.id);
  throw error;
}

const image = document.createElement('img');
image.src = await short.storage.url(file.id);
image.alt = 'Uploaded photo';
document.body.append(image);

const files = await short.storage.list();
await short.storage.remove(file.id);
```

upload accepts a nonempty File or Blob up to 10 MiB and returns {id, name, size, mimeType, createdAt}. A Blob without a filename is named file.bin. list returns files permitted by the current access policy. Fetch a display URL with storage.url(id) when rendering; store the file ID in the database. Visitor-file URLs expire after 10 minutes and authorize one file. Public-file URLs use the project's active-file transfer endpoint. The endpoint checks transfer quotas and provides CORS access for the sandbox runtime.

If record creation fails after uploading, remove the uploaded file. When deleting a post, also remove its associated files. Photo selectors use accept="image/*" and validate the selected file before uploading. Handle a missing or removed file without losing the rest of the page.

## Complete single-file example: shared guestbook

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Our guestbook</title>
  <style>
    body{font:16px/1.6 system-ui;max-width:640px;margin:40px auto;padding:0 20px}
    form,label{display:grid;gap:10px}form{gap:18px}input,textarea,button{font:inherit;padding:10px}
    li{white-space:pre-wrap;overflow-wrap:anywhere;margin:16px 0}button{cursor:pointer}
  </style>
  <script type="application/json" id="short-config">
  {"version":1,"collections":{"messages":{"access":"public"}}}
  </script>
</head>
<body>
  <h1>Our guestbook</h1>
  <form id="form">
    <label>Name<input name="name" required maxlength="30"></label>
    <label>Message<textarea name="message" required maxlength="1000"></textarea></label>
    <button disabled>Leave a message</button>
  </form>
  <p id="status" role="status">Loading…</p>
  <ul id="messages"></ul>
  <script>
    const messages = short.db.collection('messages');
    const status = document.querySelector('#status');
    const form = document.querySelector('#form');
    const button = form.querySelector('button');
    async function render() {
      const page = await messages.list({limit:20});
      const list = document.querySelector('#messages');
      list.replaceChildren();
      for (const record of page.records) {
        const li = document.createElement('li');
        li.textContent = record.data.name + ': ' + record.data.message;
        list.append(li);
      }
      status.textContent = page.records.length ? '' : 'No messages yet.';
    }
    form.addEventListener('submit', async event => {
      event.preventDefault(); button.disabled = true; status.textContent = 'Saving…';
      try {
        await messages.add({name:form.elements.name.value,message:form.elements.message.value});
        form.reset(); await render();
      } catch (error) { status.textContent = error.message; }
      finally { button.disabled = false; }
    });
    short.ready.then(async () => {
      button.disabled = false; await render();
    }).catch(error => { status.textContent = error.message; });
  </script>
</body>
</html>
```

## Quotas and retention

| Resource | Default limit |
| --- | --- |
| Projects | 5 per account |
| Deployment | Exactly one HTML file, up to 2 MiB |
| Visitor upload | 10 MiB per file |
| Storage including versions, uploads and reservations | 100 MiB per account |
| Database | 10 MiB JSON, 5,000 records and 100 collections per project |
| Single record | 16 KiB JSON |
| Uploaded files | 200 per project |
| Retained versions | Latest 3, preserving the active version |
| Database reads | 3,000 per project per day |
| Database writes | 1,000 per project per day |
| Account operations | 15,000 per day |
| Request rate | 120 per project per minute; 300 per account per minute |
| Account transfer | 1 GiB per month |
| Global operations | 150,000 per day |
| Global transfer | 20 GiB per day |

1 KiB = 1,024 bytes; 1 MiB = 1,048,576 bytes; 1 GiB = 1,073,741,824 bytes. Database size is measured from UTF-8 JSON. Daily and monthly counters reset in Asia/Seoul time. Operators can lower limits or pause the service. GET /api/sites/config returns the effective limits and enabled state.

Account and global operations include page visits, visitor sessions, management requests and file-transfer requests. Lists charge their requested page size; other requests charge at least one operation. Owner deletions used to release storage remain available when daily quotas are exhausted.

Project creation accepts retentionDays 30, 90 or 365, default 30. The user selects 1 month, 3 months or 1 year. An owner can extend by 90 days during the last 7 days. Expired and owner-deleted projects stop serving and enter a 30-day recoverable deletion period. Stored records, files and versions remain reserved against the owner quota. The owner may POST /api/sites/{site}/renew during this period to resume the same address for 90 days. Cleanup releases storage and project quota after the deadline. Operator moderation remains authoritative. Deployment upload reservations last 1 hour; visitor-file reservations last 15 minutes. Cleanup releases failed and expired reservations.

## User publishing workflow

The entire creation and management flow requires login from the beginning.

1. AI request page: the user describes a website and copies a prompt containing this manual's URL.
2. HTML page: the user selects one HTML file or pastes its code. Its <title> supplies the initial site name.
3. Site information page: the user confirms the name, chooses a 2 to 10 character Korean, Latin or numeric suffix, enters the required sharer name (1 to 10 characters) and selects retention. The login display name is the initial sharer value. Publishing creates the project and automatically connects the SDK.

My Sites provides a clickable short address with copy and QR icons, saved records, uploaded files and settings. Site Edit uses the same three pages and preloads the current HTML, title, suffix, sharer name and retention. Owners may change those fields together with the HTML while keeping project data and uploaded files. Settings has separate pages for retained versions, usage and deletion, each with a return control. Record edits use ordinary input fields while preserving structured data.

## Deployment tools

Owners can issue a 7-day development token in My Sites > Account settings > Developer settings. Store it in the deployment tool's SHORT_DEPLOY_TOKEN environment variable. Authorize HTTP requests with Authorization: Bearer <SHORT_DEPLOY_TOKEN>. The token supports the owner's site deployment and management; issue and revoke tokens using Firebase login credentials. Up to 3 active development tokens are allowed.

Use the following sequence; exact field and response schemas are in /openapi.json.

1. POST /api/sites with {title, slug, sharedBy, retentionDays} to reserve a project and suffix. Use the existing siteId when updating a project. retentionDays is 30, 90 or 365, default 30.
2. Compute the HTML byte size and lowercase SHA-256. Its path is index.html.
3. POST /api/sites/{site}/deployments with {assets:[{path:"index.html",size,sha256}]}.
4. Use deploymentId and assets[0].id to PUT the original bytes to /api/sites/{site}/deployments/{deployment}/assets/{asset}, with Content-Type: application/octet-stream. Retrying identical bytes is supported.
5. POST /api/sites/{site}/deployments/{deployment}/publish after the asset upload completes. Publication validates the bytes and inline access declaration, then atomically switches the active version. To update project metadata at the same time, send {"details":{"title":"Class album","slug":"우리사진첩","sharedBy":"Teacher","retentionDays":90}}. All four detail fields are required when details is present. A suffix conflict rejects the entire publication and preserves the previous version and metadata. A changed retention choice starts that duration at successful publication; keeping the same choice preserves the current expiry. The project ID, database records, uploaded files and visitor sessions remain the same after a suffix change. The previous suffix is retired for 30 days and the same owner may move this project back to it during that period.
6. DELETE /api/sites/{site}/deployments/{deployment} to cancel a failed upload. The previously published version stays active during an update.
Include expectedDeploymentId in the publication body to protect concurrent editors: use the currently active deployment ID, or null for a project that has never been published. If another editor publishes first, the server responds with 409 SITE_CHANGED. Reopen the project before making a new edit. A retry for the already active deployment remains idempotent.

## Error handling

SDK errors provide message, code and status. Preserve entered content and show message in a status or alert area. Use bounded retries for transient failures and offer an explicit retry action.

| HTTP status | Recovery |
| --- | --- |
| 400 | Correct the input, JSON, path or access declaration |
| 401 | Reopen the short address or refresh deployment authentication |
| 403 | Check the collection or file access policy and ownership |
| 404 | Handle a removed record or file |
| 409 | Check suffix availability, storage quotas or deployment state |
| 410 | Check publishing state and retention, then reopen the website |
| 413 | Reduce the file or record size |
| 423 | Resolve the frozen or merging account state before retrying |
| 429 | Wait for the relevant quota or rate counter to reset |
| 503 or 504 | Preserve input and retry when the service or connection recovers |

The SDK also reports HOST_UNAVAILABLE when it cannot connect to the short-address host, REQUEST_TIMEOUT for a request exceeding 35 seconds, and TOO_MANY_PENDING when 20 bridge requests are already pending. Batch and paginate work to stay within these limits.