Inkwell docs

Publish a browser game on an isolated origin, with an optional on-demand backend, database, object storage, and realtime messages.

Start with the complete first-game tutorial, consult the platform capability guide, or browse every SDK module, method and TypeScript export.

Coding agents can fetch the compact Markdown index, or a single topic such as first-game.md. The CLI exposes the same files with inkwell docs.

Packages and source

The browser SDK is optional: ordinary web games can be hosted without it. Add it for platform identity, game services, backend connections, loading and telemetry. Server-only imports belong in your creator backend, never in the browser bundle.

npm install @silicon-jungle/inkwell-sdk
npm install --global @silicon-jungle/inkwell-cli

Use npm's version list and the repositories' tagged source to pin reproducible builds. Installing an engine package does not turn a native desktop build into a browser export.

Quickstart

Sign in, open Manage games, create a game, then add page details and upload an exported build folder or ZIP. Include index.html at its root, or configure the exported HTML entrypoint.

your-build/
  index.html
  assets/
    game.js
    game.css

For the complete account, game-page, media, preview and publication sequence, follow Your first published game.

Framework builds

Inkwell hosts static browser output. Build locally or in CI, then deploy the output directory—not your source tree. A Node.js server, Express route, secret environment variable, or database connection cannot run inside a browser build. Put server logic in a separate creator backend.

Vite or React with Vite

Set a relative base so JavaScript, CSS, and imported assets load from the immutable build origin.

// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  base: './',
  plugins: [react()], // omit for non-React Vite projects
})
npm run build
inkwell deploy ./dist --game your-game-slug --publish

Three.js

Use the same Vite configuration. Imports bundled by Vite work normally. Load runtime assets with relative URLs; this works with Three.js loaders and with the optional SDK tracker.

const loader = new GLTFLoader()
loader.load('./assets/level.glb', (level) => scene.add(level.scene))

// Optional loading-bar integration:
await Inkwell.assets.trackPromise('level', loadLevel())

PixiJS

PixiJS Assets can load directly from the deployed build. Keep asset paths relative and deploy the generated dist directory.

await Assets.load('./assets/spritesheet.json')
Inkwell.ready()

Node.js build tools

Node.js is fine for bundling or generating the game. Point the CLI at the final folder containing index.html. If the project needs a server after npm start, make a static browser build first.

npm ci
npm run build
test -f dist/index.html
inkwell deploy ./dist --game your-game-slug --publish

Plain HTML

Put HTML, JavaScript, CSS, images, audio, and WebAssembly in one folder with a root index.html. No server process runs inside a game build.

Godot, Unity and other browser exports

Export your game for the web, then upload its complete output folder or ZIP from the dashboard. The CLI deploys an exported folder, not a ZIP file. Keep the generated HTML, scripts, WASM and data files together. Builds support up to 1 GiB total and 10,000 files, including individual engine files larger than 32 MiB. Large files use resumable uploads.

inkwell init --game your-game-slug --directory build/web --engine godot
inkwell deploy
# Open the preview URL, then publish the printed build ID:
inkwell publish <build-id>

The generated inkwell.config.ts describes the output and startup requirements. A plain JavaScript or MJS config works too. Set the entrypoint to the exported HTML filename.

export default {
  game: 'your-game-slug',
  client: {
    directory: 'build/web',
    entrypoint: 'index.html',
    engine: { name: 'godot', version: '4.7.2' },
    capabilities: { threads: false },
    startup: { mode: 'handshake', timeoutMs: 120000 }
  }
}

Startup and loading

Handshake mode shows Inkwell's loading screen until the game reports readiness. Include the JavaScript SDK or an engine addon in your export. The platform does not inject it into arbitrary HTML. Report progress as a number from zero to one and report startup failures with a useful message. A timeout or failure gives the player a retry button.

Inkwell.loading.progress(0.5)
// Once the first playable scene is ready:
Inkwell.ready()
// If startup fails:
Inkwell.loading.fail('The game could not load its assets. Please retry.')

For an existing export without SDK integration, choose startup.mode: 'compatible'. This reveals the game when the HTML page loads; it cannot know when the engine is playable. Legacy builds without engine metadata use this mode by default.

Engine packages

The Godot addon provides a GDScript autoload, export integration, and an example using Inkwell.game_ready(). The Unity package provides C# APIs, a WebGL JavaScript bridge, and export integration. Both expose player and game services and can call your creator backend. Their examples save progress through that backend and its database. They do not automatically upload engine-local save files or wrap every JavaScript storage API.

Install the Godot addon

Download the v0.1.1 source ZIP, copy addons/inkwell into your project, and follow the tagged README's plugin, autoload and export setup. Godot 4.7.2 GDScript Web exports have been tested in both single-threaded and threaded modes. Other engine versions and language/export combinations are not covered by that evidence.

Install the Unity package

In Unity Package Manager, add the package from this Git URL, then follow its tagged README for WebGL integration and exports:

https://github.com/siliconjungle/inkwell-unity.git#v0.1.1

Package ID: ing.inkwell.unity. The verified export is Unity 2021.3.21f1 WebGL. This does not establish compatibility with every Unity version or with threaded Unity exports.

Native gzip and Brotli exports retain their original MIME type and Content-Encoding, including application/wasm for compressed WASM. Unity's .unityweb decompression fallback files remain encoded for its loader to handle. Export compression settings must match the actual uploaded bytes.

Threads and browser requirements

Set capabilities.threads: true for an export that requires SharedArrayBuffer. Inkwell coordinates isolation headers on the player page, game responses and iframe. Unsupported environments show an explanation before launching. External resources must also satisfy the browser's cross-origin isolation rules. Single-threaded exports can leave this disabled.

Unreal and other engines

There is currently no Inkwell Unreal plugin or Unreal-to-web export toolchain. If you already have a working browser export, you can upload its HTML, JavaScript, WASM and data files and integrate the JavaScript SDK yourself. Windows and Linux packages cannot run as web builds. Inkwell does not provide Unreal Pixel Streaming, native-game hosting or GPU execution for native engines.

Progress across deployments

Store progress through your existing game backend, database or object storage. Use the trusted player identity supplied to the backend. These services belong to the game and survive new browser builds. Engine-local files, localStorage and IndexedDB are scoped to each build origin and are not automatically copied to the backend. Guest continuity depends on retaining the guest identity; the engine examples require sign-in for their account saves.

CLI

Install the public deployment CLI globally. It requires Node.js 22.13 or newer.

npm install --global @silicon-jungle/inkwell-cli

Create a key under Developer keys, then:

inkwell login
inkwell deploy ./dist --game your-game-slug --publish
inkwell whoami

CI can set INKWELL_TOKEN instead of saving a local key.

INKWELL_TOKEN=ink_sk_… inkwell deploy ./dist --game your-game-slug --publish

Complete CLI-only publication

The CLI can create and edit the page, upload media, publish builds and change visibility. No dashboard step is required.

inkwell games create --game my-game --title "My Game" \
  --visibility unlisted --cover cover.png
inkwell init --game my-game --directory dist --engine web
inkwell deploy
inkwell publish BUILD_ID
inkwell games update --game my-game --visibility public

Run inkwell docs first-game for the compact full workflow or read the rendered tutorial.

Drafts, publication and visibility

Uploading creates an immutable draft. Preview it before publishing, or use deploy --publish to publish explicitly after upload. Publishing an earlier retained build rolls the browser game back without changing its visibility, database or backend. Preview-scoped GitHub workflows always create drafts.

Private games are available to their owner and named testers. Unlisted games are playable through their link but excluded from discovery. Public games appear in discovery once published. Draft previews require owner or tester access even for a public game. Manage builds, visibility and testers on the game dashboard.

Creator backends

Each game can have one on-demand Node.js backend in its own Fly Firecracker Machine. Implement your own timers, rooms, matchmaking, small persistent world, or occasional HTTP API in that process. Inkwell does not create a process per match or impose a simulation loop. Multi-Machine scaling and creator-supplied containers are not available in v1.

Install server dependencies with npm as usual. The CLI bundles your JavaScript or TypeScript entry point and bundle-compatible dependencies into a Node.js 22 ESM module; it does not run npm install in the deployed Machine. Native addons and packages requiring extra runtime files need packaging support beyond this bundled-code interface.

// inkwell.config.js
import { defineGameConfig } from '@silicon-jungle/inkwell-sdk/config'

export default defineGameConfig({
  client: { directory: 'dist' },
  backend: {
    entry: 'server/index.js',
    region: 'syd',
    maxConnections: 100,
    resources: { memoryMb: 256, sharedCpus: 1 },
  },
})
npm install @silicon-jungle/inkwell-sdk
npm run build
inkwell deploy --game your-game-slug --publish

Database-backed account saves

Server handlers receive scoped database and storage clients plus a trusted player identity. D1 uses SQLite SQL. Validate client data and use parameterized queries. Never accept a client-supplied player ID as proof of ownership.

// server/index.js
import { defineBackend } from '@silicon-jungle/inkwell-sdk/server'

export default defineBackend({
  async start({ database }) {
    await database.query(
      'CREATE TABLE IF NOT EXISTS saves (player_id TEXT PRIMARY KEY, value TEXT NOT NULL)'
    )
  },
  async fetch(request, { database }, identity) {
    if (new URL(request.url).pathname !== '/save')
      return new Response('Not found', { status: 404 })
    if (request.method === 'GET') {
      const result = await database.query(
        'SELECT value FROM saves WHERE player_id = ?', [identity.playerId]
      )
      return Response.json(result.results[0] ?? null)
    }
    if (request.method !== 'PUT')
      return new Response('Method not allowed', { status: 405 })
    const input = await request.json()
    if (!input || typeof input.value !== 'string' || input.value.length > 10000)
      return new Response('Invalid save', { status: 400 })
    await database.query(
      'INSERT INTO saves (player_id, value) VALUES (?, ?) ON CONFLICT(player_id) DO UPDATE SET value = excluded.value',
      [identity.playerId, input.value]
    )
    return Response.json({ saved: true })
  },
})
// Browser code, inside the Inkwell player frame
import { Inkwell } from '@silicon-jungle/inkwell-sdk'

const response = await Inkwell.backend.request('/save', {
  method: 'PUT',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ value: 'my checkpoint' }),
})
if (!response.ok) throw new Error('Save failed')

Backend identity.playerId is stable within a game for a signed-in account across sessions and devices. Guest IDs follow the platform player cookie; clearing it or signing in changes the identity. Presence and game-chat routing use the same game-scoped player identity; individual transport connections have separate IDs. Neither is a credential, and account emails are not exposed.

World data and shared objects

D1 can also hold guilds, inventories, world records, or any other creator-defined tables. Each game has one private R2 namespace, shared by all its worlds. World IDs label storage for quota accounting; they do not create separate servers or authorize access.

// Inside a server handler with context and a validated worldId:
await context.storage.put('worlds/' + worldId + '/snapshot.json',
  JSON.stringify(snapshot), { contentType: 'application/json', worldId })
const saved = await context.storage.get('worlds/' + worldId + '/snapshot.json')
const snapshotAgain = await saved.json()
const page = await context.storage.list({ prefix: 'worlds/' + worldId + '/' })
// Follow page.cursor when page.truncated is true.
// context.storage.delete(key) removes an object.
// Omit worldId for game-wide assets shared by every world.

Only the backend can access D1 and private R2 objects. Expose authorized reads through your server handlers when clients need them. Immutable public assets can instead ship in the browser build. Machine memory and local files are not durable game storage: restore from D1/R2 on start and save important changes as they happen, not only during shutdown. New browser builds have different origins, so IndexedDB and localStorage are not cross-deployment account saves.

Messages, actions and lifecycle

defineBackend supports start, connect, disconnect, messages, actions, fetch, and shutdown. Message handlers receive payload, connection, context, and delivery mode. Action handlers return a reliable request/response result.

const connection = await Inkwell.backend.connect()
await connection.sendReliable('chat.send', { text: 'Hello' })
connection.sendUnreliable('cursor.move', { x: 10, y: 20 })
// Register matching handlers in your server definition:
// messages: { 'chat.send': async (payload, connection, context, delivery) => { ... } }
// actions: { 'world.join': async (payload, connection, context) => { ... } }

Pooled game servers accept WebTransport directly with SDK 0.0.6 or newer. Reliable streams and unreliable QUIC datagrams terminate inside the game server, without a shared gameplay gateway. The SDK reports native delivery for direct connections and emulated delivery for legacy servers still using a TCP gateway hop. Direct connections require WebTransport and do not fall back to WebSocket. Game simulation, prediction and reconciliation remain creator code.

Opening a backend-enabled game starts its server. Idle players and HTTP-only games keep it running through player-frame heartbeats; open realtime connections also retain it. After the final player leaves or their presence expires, there is a five-minute shutdown grace period. Presence leases expire after 90 seconds without a heartbeat. maxConnections caps concurrent transport connections, including in-flight backend HTTP calls; it is not a promised player capacity or automatic scale-out setting.

Server-only API keys

inkwell secrets set SERVICE_API_KEY --game your-game-slug
inkwell secrets import .env.production --game your-game-slug
inkwell secrets list --game your-game-slug
inkwell secrets unset SERVICE_API_KEY --game your-game-slug

You can also manage secrets from the game dashboard. Values are encrypted in the per-game Fly vault and injected into server process.env on the next boot. Listing returns names, never values. Secret changes do not interrupt connected players. Browser uploads reject secret files; never put secrets in client code or return them from a handler. Creators with server deployment access can read their own game secrets. Public outbound internet requests are allowed; platform credentials are not provided.

GitHub branch deployments

On your game dashboard, connect GitHub, select a repository, and set the production branch and exact workflow path. Add the workflow below at .github/workflows/inkwell.yml. Change main to match your selected branch. Pushes then test, build and deploy without a long-lived Inkwell API key.

name: Deploy to Inkwell
on:
  push:
    branches: [main]
  workflow_dispatch:
permissions:
  contents: read
  id-token: write
concurrency:
  group: inkwell-production
  cancel-in-progress: false
jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
        with:
          node-version: '22.13.0'
          cache: npm
      - run: npm ci --no-audit
      - run: npm audit --audit-level=high
      - run: npm test
      - run: npm run build
      - run: npx --yes @silicon-jungle/inkwell-cli deploy --game your-game-slug --publish
        env:
          NPM_CONFIG_AUDIT: 'false'

GitHub signs the job identity; Inkwell checks the connected repository, workflow and branch, then issues a short-lived scoped deployment credential. Connecting a repository does not itself add the workflow file. Optional non-production previews deploy browser builds only and leave the live backend unchanged. Backend updates may queue until the current server stops; keep client and server protocol changes compatible during that transition.

API

Creator automation can send an API key as a Bearer token. Some account operations require a signed-in session instead. JSON errors use an error field. See the API audience and authentication guide before using runtime, session-only or internal endpoints.

Complete request bodies, response shapes and curl examples are in the Creator API guide.

Game creation, management, deployment, private creator analytics, and developer-key generation require an approved creator or administrator account.

Authorization: Bearer ink_sk_…
Content-Type: application/json
  • GET/POST /api/v1/games — list or create games.
  • GET/PATCH/DELETE /api/v1/games/:slug — manage metadata.
  • POST /api/v1/games/:slug/media?kind=cover|screenshot — upload an image body.
  • POST /api/v1/games/:slug/builds — create a manifest-based deployment.
  • GET /api/v1/games/:slug/builds — list retained builds and publication state. GET/POST/DELETE /api/v1/games/:slug/testers — list, add or remove named testers; mutations accept a public username. Tester access does not grant creator permissions.
  • PUT /api/v1/builds/:id/files and POST /api/v1/builds/:id/finalize — upload and finish a draft. Finalize accepts an explicit publish flag; publication otherwise uses POST /api/v1/builds/:id/publish. Use multipart PUT for batches, or a raw PUT body with ?path=... for a single file.
  • GET/PUT/POST /api/v1/builds/:id/chunks — resumable large-file upload protocol. Prefer the CLI or dashboard: they handle the manifest, chunk state and upload verification.
  • GET/POST /api/v1/games/:slug/backend — inspect or provision/configure the owned game backend. GET/POST /api/v1/games/:slug/backend/deployments — list or submit bundled backend deployments. Use the CLI to bundle code and dependencies.
  • GET/POST /api/v1/games/:slug/backend/secrets — list secret names or set server values; DELETE /api/v1/games/:slug/backend/secrets/:name removes one. Never call these with creator credentials from a game iframe.
  • GET/POST/DELETE /api/v1/games/:slug/github — inspect, configure or disconnect the repository/branch/workflow mapping with a creator session. GET /api/v1/github/installations and /api/v1/github/installations/:id/repositories list available installations and repositories. GitHub callbacks, webhooks and Actions exchange use their own verified protocols; use the documented workflow rather than a browser game.
  • POST /api/v1/games/:slug/leaderboards, /achievements, /stats and /chat — creator management of game services. The dedicated service guides document operation payloads and policies; browser games should use the scoped SDK instead.
  • PUT/DELETE /api/v1/games/:slug/favorite — favourite a game.
  • PUT/DELETE /api/v1/games/:slug/reviews — create, update, or delete your review.
  • PUT/DELETE /api/v1/creators/:username/follow — follow a creator.
  • GET /api/v1/catalog/games?q=… and /catalog/games/:slug — browse public games and their store-page data.
  • GET /api/v1/creators/:username — public creator, published games, favourites, follower count, and total playtime.
  • GET /api/v1/games/:slug/analytics?days=30 — private creator analytics using a session or API key.
  • GET /api/v1/games/:slug/presence — current play count, safe player profiles, and signed-in friends currently playing.
  • GET /api/v1/games/:slug/friends — accepted friends who have played the game, including playtime and current activity.
  • GET /api/v1/friends and /friends/:username — list, request, accept, decline, or remove friends. Accepted friends include recent public-game activity and playtime.
  • GET /api/v1/me/playtime or ?game=:slug — inspect your total, recent games, or playtime for one game.
  • GET/POST /api/v1/messages/:username — read or send friend-only direct messages.
  • DELETE /api/v1/messages/:username/:messageId — delete a message you sent.
  • GET/PUT/DELETE /api/v1/blocks/:username — inspect, block, or unblock an account; GET /api/v1/blocks lists accounts you blocked.
  • GET/POST /api/v1/reports — submit a game, profile, review, or received-message report and inspect your reports.
  • GET/PATCH /api/v1/notifications — list notification and unread counts or mark notifications read.
  • POST /api/v1/games/:slug/invites and POST /api/v1/games/:slug/invites/:inviteId/accept — session-only friend invitations and recipient acceptance. These are platform UI operations, not creator/runtime-key permissions. See the invite contract for SDK context hooks and limits.
  • POST /api/v1/game-services/presence — runtime-only current-game count and bounded public roster, with no friends list or cross-game selector. Browser games use Inkwell.presence.get() through the host instead.
  • GET/PATCH/DELETE /api/v1/account, /account/avatar, and /account/export — session-only account settings, avatar, export, and deletion.
  • GET /api/v1/me — inspect the authenticated creator and API-key identity.
  • GET/POST /api/v1/keys, POST /api/v1/keys/:id/rotate, and DELETE /api/v1/keys/:id — create, rotate, list, or revoke developer keys with a signed-in session.

Machine-readable limits are available at GET /api/v1/limits.

Game SDK

The optional public SDK reports readiness, completion, loading progress, private creator analytics, and a minimal safe player profile. The SDK's displayName field contains the player's public username for compatibility; it never exposes a private name, email, account ID, cookie, or session token to a game.

npm install @silicon-jungle/inkwell-sdk
import { Inkwell } from '@silicon-jungle/inkwell-sdk'

const player = await Inkwell.player.get()
// { displayName, avatarUrl, isGuest }
// displayName is the player's public username

const online = await Inkwell.presence.get()
// { total, guestCount, players, friends }

const stopMonitoring = Inkwell.performance.start()

Inkwell.ready()
Inkwell.analytics.track('level.complete', { level: 3 })
Inkwell.session.complete()
stopMonitoring()

Modular imports

Import only the module a game needs, or use the single Inkwell object shown above.

import { ready } from '@silicon-jungle/inkwell-sdk/core'
import { complete } from '@silicon-jungle/inkwell-sdk/session'
import { track } from '@silicon-jungle/inkwell-sdk/analytics'
import { trackedFetch, defaultTracker } from '@silicon-jungle/inkwell-sdk/assets'
import { get as getPlayer } from '@silicon-jungle/inkwell-sdk/player'
import { get as getPresence } from '@silicon-jungle/inkwell-sdk/presence'
import { start as monitorPerformance } from '@silicon-jungle/inkwell-sdk/performance'

Loading progress

Normal relative URLs and engine loaders continue to work. Import@silicon-jungle/inkwell-sdk/assets only when the game wants a shared loading bar. trackedFetch uses byte progress when the server provides a content length; otherwise it falls back to completed asset count.

const unsubscribe = defaultTracker.subscribe(({ ratio }) => {
  loadingBar.value = ratio ?? 0
})

const response = await trackedFetch('./assets/level.glb')
const level = await response.arrayBuffer()
unsubscribe()

Player and presence privacy

Presence player IDs are opaque and scoped to the current game. Games receive public display profiles only, never account IDs, email addresses, cookies, or authentication tokens.

Performance samples

Performance monitoring is opt-in and sends bounded aggregate samples every 30 seconds: FPS, p95/max frame time, long tasks, visibility, and heap use where supported. It does not send raw traces, resource URLs, or hardware fingerprints.

Free MVP limits

  • 120 API requests and 30 writes per account per minute.
  • 10 new games and 100 deployments per account per UTC day.
  • 1 GiB of build files, up to 10,000 files. Individual large files use resumable 8 MiB chunks; the folder and ZIP uploaders use the same limit.
  • Five retained builds per game and 5 GiB stored builds per account. Administrators have unlimited aggregate build, backend, page-image and private-object storage across projects. Individual upload, per-game and per-world limits still apply. Authenticated API limit responses use null for unlimited storage.
  • Eight page images per game, 5 MB each and 25 MB total.
  • 250 MB of page images across each creator account.
  • Five genre tags, 240-character summary, and 20,000-character Markdown page.
  • Approved creator and administrator accounts may have ten current developer API keys. A rotated predecessor remains usable for a 24-hour migration window.
  • 20 direct messages per minute, 500 per UTC day, 30 friend requests, and 20 reports per rolling 24 hours.
  • Avatars are JPEG, PNG, WebP, or GIF and at most 5 MB.
  • Backend bundles: 10 MiB each; 500 MiB retained per creator.
  • Backend HTTP request and response bodies: 8 MiB each.
  • Private R2 objects: 100 MiB each, 500 MiB per world, 1 GiB per game, and 5 GiB per creator. These are separate from build storage.
  • D1 batches: 20 statements, up to 100 parameters per statement.
  • Server secrets: 50 per game, 64 KiB each, 256 KiB combined.

Game security

Every uploaded build runs on a distinct b…-g….inkwellgame.com origin. The player iframe allows scripts, same-origin access within that isolated build, pointer lock, orientation lock, fullscreen, autoplay, and gamepad access. Camera, microphone, geolocation, payments, and USB are disabled by the game gateway.