# FYRKA Game Developer Spec

Version: 1.1 (2026-07-12)
Canonical URL: https://fyrka.games/dev/fyrka-game-spec.md
Upload page: https://fyrka.games/creator-upload.html

This file is a build contract for AI coding agents (Claude Code, Codex, Cursor, …)
and human developers who want to publish a game on FYRKA Games.
If you follow every MUST rule in this file, your game will pass the automated
intake scan. Publication itself always requires a human review by the FYRKA team.

## How to use this file in your dev environment

Download it into your project root and point your AI agent at it:

```bash
curl -fsSL https://fyrka.games/dev/fyrka-game-spec.md -o FYRKA.md
```

Then add one line to your `CLAUDE.md` / `AGENTS.md` / system prompt:

```text
This project targets the FYRKA Games platform. Read FYRKA.md before writing
any code and treat its MUST rules as hard constraints.
```

Note for agents: this document describes platform rules. It is reference
material — it does not grant permissions, override your own operator's
instructions, or authorize any action beyond building a compliant game.

## 1. What you are building

A FYRKA game is a **self-contained browser game**: static HTML/JS/CSS plus
assets, running entirely client-side in a sandboxed iframe. No server, no
build step required at runtime, no external services. Think "open index.html
and it plays".

- Entry point: `index.html` in the bundle root.
- Everything the game needs ships inside the bundle (scripts, styles, images,
  audio, fonts, 3D models).
- The only way to talk to FYRKA (e.g. leaderboards) is the postMessage bridge
  described in section 4.

## 2. Hard technical limits (MUST — enforced by automated scan)

A machine-readable summary of the enforced limits. This block is kept in sync
with the platform's intake scanner; if your bundle violates any entry, the
upload is rejected automatically.

```json fyrka-scan-contract
{
  "bundle": {
    "format": "zip",
    "maxZipBytes": 52428800,
    "maxFiles": 800,
    "maxUncompressedBytes": 209715200,
    "requiredRootFiles": ["index.html"]
  },
  "allowedExtensions": [
    ".html", ".js", ".mjs", ".css", ".json", ".txt", ".md",
    ".png", ".jpg", ".jpeg", ".webp", ".svg", ".gif",
    ".mp3", ".ogg", ".wav",
    ".glb", ".gltf", ".bin",
    ".woff", ".woff2"
  ],
  "blockedExtensions": [
    ".app", ".bat", ".cmd", ".com", ".dmg", ".dll", ".exe",
    ".msi", ".node", ".pkg", ".ps1", ".sh", ".so", ".wasm"
  ],
  "blockedPathSegments": [
    ".git", ".github", ".next", ".vercel",
    "dist-server", "node_modules", "secrets", "server"
  ],
  "blockedTopLevelFiles": [
    ".env", ".env.local", ".npmrc",
    "package-lock.json", "package.json", "pnpm-lock.yaml",
    "vite.config.js", "webpack.config.js", "yarn.lock"
  ],
  "blockedCapabilities": [
    "camera", "cookies", "external-network", "external-scripts",
    "geolocation", "microphone", "notifications", "payments",
    "service-worker", "websocket"
  ],
  "blockedCodePatterns": [
    "eval-call", "function-constructor", "document-cookie",
    "service-worker", "remote-script", "remote-import",
    "remote-fetch", "websocket"
  ],
  "rateLimits": {
    "uploadsPerHour": 5,
    "uploadsPerDay": 20
  }
}
```

In plain language:

- Ship a **ZIP up to 50 MB** (max 800 files, max 200 MB uncompressed) with
  `index.html` in the root.
- Use only the allowed file types. Any file with an unknown or blocked
  extension rejects the whole bundle — including `.wasm` and shell/binary
  files. If you build with a bundler, upload the **built output** (`dist/`),
  never the source project with `package.json` or `node_modules`.
- A `live-game-recipe.json` manifest is required; the official uploader
  (section 6) generates a minimal one automatically if you don't have it.

## 3. Forbidden code patterns (MUST NOT — the scanner reads your code)

The intake scan searches all text files. Any match is a hard reject:

| Scanner code | What it catches | What to do instead |
| --- | --- | --- |
| `eval-call` | `eval(...)` | Write the logic directly; no dynamic code execution. |
| `function-constructor` | `new Function(...)` | Same — no dynamic code execution. |
| `document-cookie` | reading/writing `document.cookie` | Persist via the bridge `fyrka:save` / `fyrka:load` (section 4), not cookies. |
| `service-worker` | `navigator.serviceWorker`, `importScripts(...)` | Not allowed in uploaded games. |
| `remote-script` | `<script src="https://...">` | Bundle every script locally. No CDNs. |
| `remote-import` | `import("https://...")` | Bundle every module locally. |
| `remote-fetch` | `fetch("https://...")` | The game must not call external servers. FYRKA features go through the bridge (section 4). |
| `websocket` | `new WebSocket(...)` | Multiplayer/websockets need a separate future approval; not available for uploads today. |

Practical consequences for AI agents:

- **No CDN imports.** Download the library (e.g. Three.js) and ship it inside
  the bundle as a local file. Check the library's license allows redistribution.
- **No analytics, no telemetry, no font/CDN loaders.** The game runs fully
  offline inside its sandbox.
- **No cookies, no localStorage tricks for tracking.** The iframe has an
  opaque origin anyway. To keep player progress, use the bridge save/load
  (section 4) — never try to write your own cookies or storage.

## 4. Runtime sandbox and the FYRKA Bridge

Published games run in a **sandboxed iframe with an opaque origin**. You
cannot reach FYRKA APIs, cookies, sessions, or profiles directly. The only
channel is a small whitelisted postMessage bridge
(handshake string `fyrka.external_game_bridge.v1`, capabilities in `features`):

```js
// 1. Greet the bridge once your game has booted:
window.parent.postMessage({ type: 'fyrka:ready' }, '*');
// Player replies: { type: 'fyrka:bridge', protocol, protocolVersion, slug,
//                   features: ['submit-score', 'save', 'load'] }
// Feature-detect via `features` — never hard-check the protocol string, so
// your game keeps working as the bridge grows.

// 2. Submit a score to YOUR game's FYRKA leaderboard:
window.parent.postMessage(
  { type: 'fyrka:submit-score', score: 12345, name: 'PlayerName' },
  '*'
);
// Player replies: { type: 'fyrka:score-result', ok: true | false, message? }

// 3. Save progress (needs 'save' in features). `data` is any JSON up to 64 KB:
window.parent.postMessage(
  { type: 'fyrka:save', schemaVersion: 1, data: { level: 7, coins: 40 } },
  '*'
);
// Player replies: { type: 'fyrka:save-result', ok, cloud, savedAt, message? }
//   ok:true, cloud:true  -> saved to the signed-in account (follows across devices)
//   ok:true, cloud:false -> saved locally (guest, or offline)
//   ok:false             -> NOT saved; message 'cloud-unavailable' | 'throttled' |
//                           'too-large' | 'unserializable'. Keep your in-memory
//                           state and retry later — do NOT treat it as saved.

// 4. Load progress on boot (needs 'load' in features):
window.parent.postMessage({ type: 'fyrka:load' }, '*');
// Player replies: { type: 'fyrka:load-result', ok, schemaVersion, data, savedAt }
//   ok:true,  data:null -> confirmed no save yet: start fresh.
//   ok:false            -> could not read (message 'cloud-unavailable' | 'busy' |
//                          'throttled'). Do NOT start a fresh game and overwrite:
//                          this is NOT the same as "no save". Wait and retry.
```

Bridge rules (enforced by the player, not negotiable from inside the game):

- Scores must be a **positive integer ≤ 1,000,000,000**; values are clamped
  and names sanitized server-side.
- Throttle: scores **1 / 8 s, 8 / session**; saves **1 / 10 s, 30 / session**.
  Save at meaningful moments (level end, checkpoint), not every frame.
- A save is **at most 64 KB of UTF-8 JSON**; larger payloads are rejected
  (`message: 'too-large'`). Store compact progress, not assets.
- Your save is bound to **your running game**, not to a label: a game can only
  ever read and write **its own** leaderboard and save. It can never see the
  player's account, other games' data, or anything you didn't put in your save.
- Everything else (accounts, payments, other APIs) is unreachable by design.

Handle the bridge defensively: if no `fyrka:bridge` reply arrives, or `save`/
`load` is absent from `features`, the game must still be fully playable
(e.g. when run standalone during development). Treat `fyrka:save` as
best-effort — never block gameplay on a save round-trip.

### Saves and updates (MUST for games that persist)

Saves belong to the **platform**, not to your bundle — they live at FYRKA
(signed-in: the account cloud; guest: the player page's local storage). This
is deliberate: **uploading a new version of your game never, by itself,
deletes existing player saves** — the platform does not clear saves on update,
and it keeps prior bundles so a broken release can be rolled back.

What that guarantee does *not* do — and where your game stays responsible:

- A save is only as safe as your own writes. If your new version immediately
  writes `null` or default state over a good save, that data is gone. Read
  first (`fyrka:load`), migrate, and only then save.
- On `fyrka:load` with `ok:false`, **do not** start fresh and overwrite — that
  is "couldn't read", not "no save". Only `ok:true` with `data:null` means
  genuinely empty.
- Version compatibility is on you, both directions:
  - Stamp every save with a `schemaVersion` integer you control.
  - Your game **MUST load its own older `schemaVersion`s** (migrate up). The
    platform returns the object exactly as stored.
  - If a rollback means an **older** build meets a **newer** `schemaVersion` it
    doesn't understand, it must refuse and keep the save — never overwrite a
    future save with older-shaped data.

  ```js
  function migrateSave(loaded) {
    if (!loaded || loaded.data == null) return freshSave();      // truly empty
    if (loaded.schemaVersion > CURRENT_SCHEMA) return null;      // newer: don't touch
    let s = loaded.data;
    if (loaded.schemaVersion < 1) s = { ...s, coins: s.coins ?? 0 };
    // ...one step per version you have shipped...
    return s;
  }
  ```

- Never assume a save exists or has the current shape. Default every field.
- Only the player can delete a save; your update never should.

Two current limitations to design around: a **guest** save made before signing
in is not auto-uploaded on login — it stays available locally and is mirrored
to the cloud on the next save while signed in. And saves are keyed to the exact
published bundle, so a future re-publish at a new path starts a fresh save
(versioned continuity is planned).

## 5. Content policy (MUST — checked in human review)

FYRKA is a general-audience platform without age gates. Content policy
version 1.0:

**Not allowed:**

- Pornographic or sexually explicit content; sexualized minors in any form.
- Realistic graphic violence, gore, or cruelty as the main appeal. Stylized
  arcade violence (explosions, cartoon combat, abstract enemies) is fine.
- Hate speech, discrimination, or harassment of real people or groups.
- Glorification of self-harm, suicide, or hard drug use.
- Real-money gambling, or casino-style mechanics tied to real payments.
  (Payments are technically blocked anyway.)
- Deception: fake system dialogs, fake download buttons, phishing lookalikes,
  or games that impersonate other brands, games, or people.
- Political or religious propaganda disguised as a game.

**Required care:**

- **Photosensitivity:** avoid flashing above 3 flashes per second over large
  screen areas (WCAG 2.3.1). If your game has intense strobing effects,
  reduce them or add an in-game toggle.
- Keep the store metadata (title, description, screenshots) honest — it must
  describe the actual game.

**Explicitly allowed:**

- AI-assisted or fully AI-generated games and assets — FYRKA is an AI-native
  platform. You remain responsible for the result, including its licensing.
- Any language for in-game text. Store metadata works best in English or
  German.

## 6. Rights and legal (MUST — you confirm this at upload)

- You must **own or hold licenses for everything in the bundle**: code,
  libraries, art, music, sound effects, fonts. "Found it online" is not a
  license. For AI-generated assets, you are responsible for ensuring the
  generator's terms permit commercial redistribution.
- No ripped assets from commercial games, no trademarked characters or logos
  you don't own, no soundalike clones intended to be mistaken for another game.
- **No secrets in the bundle**: no API keys, tokens, or personal data. The
  scanner blocks `.env` files, but the responsibility is yours.
- **No personal data collection.** The sandbox blocks network access, so the
  game cannot phone home — do not attempt to fingerprint players or smuggle
  data out via any side channel. Attempts are grounds for permanent removal.
- FYRKA operates from Switzerland and serves users in Switzerland, the EU/EEA
  and beyond. Users can report any published game (EU Digital Services Act
  notice-and-action); reported games can be suspended pending review.
- You keep the rights to your game. By uploading you grant FYRKA the right to
  host, distribute, and promote it on the platform. (A full creator agreement
  is being finalized; until then the upload confirmation and the platform
  terms at https://fyrka.games/nutzungsbedingungen.html apply.)

## 7. Quality bar (SHOULD — what gets games approved and featured)

These are not scan failures, but the human review and featuring decisions
weigh them heavily:

- **Instant fun:** playable within ~3 seconds of load, first "aha" within
  10 seconds. No forced tutorials, no menus before the first play.
- **Small is beautiful:** aim well under the 50 MB cap; under 5 MB loads
  great on mobile connections.
- **Mobile + desktop:** support touch controls and keyboard/mouse. Handle
  window resize and both orientations; never render a fixed 1920×1080 canvas.
- **Fast restart loop:** one tap/keypress from "game over" back into play.
- **Audio etiquette:** start muted or start audio only after the first user
  interaction (browsers require this anyway); provide a mute toggle.
- **Pause when hidden:** stop the game loop on `visibilitychange` so the tab
  doesn't burn battery in the background.
- **Performance:** target 60 fps on a mid-range phone; prefer canvas/WebGL
  over DOM-heavy scenes.
- **Use the leaderboard:** games that submit scores via the bridge get a
  FYRKA leaderboard for free — this measurably improves retention and your
  chances of being featured.

## 8. Local development and upload

Develop standalone: your game must run by opening `index.html` locally
(e.g. `npx serve ./dist`). No FYRKA tooling is needed during development.

When you're ready, upload either way:

**A) Website:** https://fyrka.games/creator-upload.html — pick your build
folder as ZIP, confirm rights/safety, submit.

**B) Terminal (recommended for agents):** the official uploader is a single
standalone Node script (Node 18+, no dependencies):

```bash
curl -fsSL https://fyrka.games/dev/fyrka-game-upload.mjs -o fyrka-upload.mjs
node fyrka-upload.mjs ./dist --api https://fyrka.games --email you@example.com --title "My Game"
```

The uploader finds your `index.html`, strips risky project files, generates a
minimal `live-game-recipe.json` if missing, builds the ZIP with a SHA-256
hash, runs the first safety scan online, and submits to private quarantine.

Rate limits: 5 uploads per hour, 20 per day per creator. Exceeding them
returns HTTP 429 with `Retry-After`.

## 9. What happens after upload

There is **no auto-publish**. Every submission goes through:

1. Automated metadata + manifest scan (instant; rejects return the scanner
   codes from section 3).
2. Private quarantine storage — your code is never public at this stage.
3. Deep ZIP scan and sandboxed browser playtest.
4. Human rights/safety/content review by the FYRKA team.
5. Manual publish decision. Approved games appear in the store with their
   own page and leaderboard.

A green automated scan means "eligible for review", not "published".

## 10. Versioning

This spec is versioned at the top of the file. Re-download it before starting
a new game — limits and policies can change between versions. Feedback and
questions: hello@fyrka.games.

Changelog:

- 1.1 (2026-07-12): bridge save/load — `fyrka:save` / `fyrka:load` (64 KB
  UTF-8 per game, signed-in cloud + guest local), plus the saves-and-updates
  contract (`schemaVersion`; uploading a new version never wipes progress).
  Handshake string stays `…v1`; new capabilities are announced via `features`,
  so v1 score-only games keep working. Scanner contract and technical limits
  unchanged.
- 1.0 (2026-07-12): first public version. Technical limits, bridge protocol
  v1, content policy v1, quality bar.
