com.robotsquid.squidhub
v0.5.13
Published
Cloud saves, analytics and the coin economy for Unity WebGL games published on SquidHub. Installs the C# wrapper, the .jslib half and the page bridge, and rewrites the exported index.html so it survives the platform's CSP.
Maintainers
Readme
SquidHub for Unity
Cloud saves, analytics and the coin economy for Unity WebGL games on SquidHub.
Install it, press Build. There is no bundling step, no WebGL template to pick
and nothing to copy into Assets/.
Install
Add the registry to Packages/manifest.json:
{
"scopedRegistries": [
{
"name": "RobotSquid",
"url": "https://registry.npmjs.org",
"scopes": ["com.robotsquid"]
}
]
}Then Window > Package Manager > My Registries > SquidHub > Install.
Unity 2021.3 or newer.
Use it
using System.Collections.Generic;
using SquidHub;
async void Start()
{
await Sdk.WaitForReadyAsync(); // throws if the portal refuses
var json = await Sdk.SavesGetAsync(); // JSON; parse with your own library
// `data` inside it is base64 — Sdk.FromBase64(...) gives you the bytes.
Sdk.AnalyticsTrack("level_done", new Dictionary<string, object> { ["level"] = 3 });
await Sdk.SavesPutAsync(bytes, ifVersion: version);
}Responses come back as JSON strings deliberately: your project already carries a JSON library, and a second one shipped in here would be a worse one.
The full API — capabilities, save conflicts, error codes, the manifest — is
documented in @squidhub/game-sdk.
This package is a wrapper around it and shares its behaviour exactly.
⚠️ Handle CONFLICT on SavesPutAsync. It is not an edge case — it is what
happens whenever a player has your game open in two places, which is the
situation cloud saves exist for. The server's copy rides along on the error so
you can merge without a second round trip. The platform stores opaque bytes and
cannot merge for you.
What happens when you build
Nothing to configure. On a WebGL build this package:
- Refuses the build if it could not possibly run — pthreads enabled, the
.jslibnot linked for WebGL, the bridge missing. Each of those otherwise fails silently in a browser rather than loudly in the Editor. - Writes
squidhub-bridge.jsinto the output and injects it ahead of the Unity loader. - Hoists every inline
<script>out ofindex.htmlinto an external file. - Makes the game fill the frame and hides Unity's own logo bar, build title and fullscreen button.
Step 3 is the one that matters. squidhub-gamehost serves
script-src 'self' 'wasm-unsafe-eval' blob: with no 'unsafe-inline', and
every stock Unity template inlines its bootstrap. Left alone, the page loads,
all the markup is there, and nothing runs — the only signal is a CSP violation
in the console. This is the single most common first-integration failure, and
you should never see it.
⚠️ Step 4 is why your game is not a 960×600 box in the corner. The portal
already letterboxes — it measures its box, clamps it to the aspect window you
declared and hands the frame an element of exactly that size. Unity's stock
template then renders a fixed 960×600 centred inside that, on a white page,
under a Unity logo and a second fullscreen button that fights the portal's own.
The footer is hidden rather than removed, because the stock bootstrap queries
the fullscreen button and throws on null — which surfaces as an alert() where
the game should be. Your loading bar is left alone: the portal draws none, so it
is the only progress a player sees.
Then push the folder:
node src/cli.mjs push --game <slug> --dir <BuildFolder> --env=dev --verifyDeclaring what your game is
SquidHub → Game Manifest… edits manifest.json beside Assets/ — the game's
id and title, which capabilities it uses, its save schema version, the
orientations and aspect window it wants, and whether it makes sound.
It is the same file squidhub-release build --manifest manifest.json reads, so
the Editor and the CLI cannot disagree about what your game declares. It belongs
in your repository.
The build writes it into the output folder for you. That is the only thing
that makes a declaration take effect — the server reads manifest.json out of
the uploaded bytes and ignores anything a client posts alongside the upload, so
a build folder without one declares nothing at all: save schema 1, no display
constraints, and a rollback guard that cannot fire.
⚠️ Ticking a capability does not grant it. Capabilities are granted by the
platform at review. Declaring commerce.spend says your game uses it; until it
is granted the broker still answers DENIED, and your game has to handle that.
New games have saves and analytics.
⚠️ Keys the dialog does not model are preserved. skus, achievements and
rewardTable live in the same file and survive a save untouched.
Uploading from Unity
SquidHub → Upload Build… does the whole publish — no console, no command line,
no repository checkout. SquidHub → Build and Upload… picks a build profile,
builds it and uploads the result in one action.
Once: in the publisher console open your game → Upload keys → create one,
then paste it into SquidHub → Upload Settings.
⚠️ The key never enters your project. It lives in EditorPrefs, per machine and per project path, so it cannot reach your repository. It can upload a build for the one game it was minted for, and on dev publish that build — it cannot read anything, touch another game, or publish on prod. If it leaks, revoke it in the console.
Publishing on upload
Publish the build is ticked by default in SquidHub → Build and Upload….
⚠️ It works on dev only. On prod the server refuses it and says so; the upload still succeeds and the build arrives registered and not live. Promoting a build for every player is an owner decision made by a person in the console, and an upload key is not a person — it sits in CI configs and Editor preferences. That asymmetry is deliberate: it is what keeps a leaked key from replacing the live build for everyone.
⚠️ It does not ask. Choosing the action is the decision, so there is no "upload N files?" confirmation to dismiss — the file count, size, destination and any warnings go to the Console instead, where they survive being read. A build that cannot upload still stops with a dialog naming the fix.
The upload is refused before a byte moves if the build is precompressed or has
no index.html at its top level, and OS metadata is dropped rather than sent —
which is also what makes a build pushed from Unity hash identically to the same
bytes dragged into the console.
In the Editor
Sdk compiles on every platform and reports Status() == "failed" anywhere
that is not a WebGL build — pressing Play does not throw.
⚠️ It never simulates a session. There is no offline player, no fake save and no pretend balance, because a game built against a player who cannot exist gets its first honest answer from production. Your game must start when a platform call fails; treat every verb as something that can fail and keep a local fallback.
Run SquidHub > Check WebGL Settings any time to audit the project without waiting for a build.
Player Settings
The package checks these for you, but for reference:
| Setting | Value | Why |
|---|---|---|
| Native C/C++ Multithreading | off | SquidHub's default tier is not cross-origin isolated, so SharedArrayBuffer does not exist. A pthreads build cannot start. Ask about the isolated tier if you genuinely need threads — it carries no ads |
| Run In Background | on | ⚠️ Unity WebGL pauses when the canvas loses focus, and every control on SquidHub — mute, fullscreen, back — belongs to the portal, not the game. Left off, clicking any of them stops the game; fullscreen then keeps rendering at the pre-fullscreen resolution, so the game is blurry until the player clicks into it |
| Compression Format | Disabled | ⚠️ Required, and the build is refused otherwise. SquidHub stores assets uncompressed and Cloudflare compresses them at the edge, per client. A precompressed build cannot be served correctly at all |
Things that will otherwise bite
⚠️ Your game only works when the portal launches it. The launch nonce
arrives in the URL fragment (#s=…), so opening index.html directly, or
reloading without the fragment, fails the handshake with UNTRUSTED_EMBEDDER.
That is intended, not a bug to work around.
⚠️ No external resources, at all. The CSP is default-src 'self' blob:
data:. A CDN font, a remote texture or a third-party analytics snippet is
blocked at load and shows up as a console error, not as a missing style.
Everything must ship inside the build.
⚠️ Fullscreen needs two things, and neither is the Unity fullscreen button.
Tick Fullscreen button in SquidHub → Game Manifest… — display.fullscreen
defaults to false and the portal draws no control without it — and turn on Run
In Background, or the game pauses the moment the player clicks that control and
stays at the old resolution while the frame grows. The portal fullscreens its own
container rather than asking your game to do it, so your build needs no fullscreen
code at all; Unity's own button is hidden for you.
⚠️ Do not write your own rotate prompt or call screen.orientation.lock().
Declare the orientations you support in SquidHub → Game Manifest… and the
portal draws the prompt outside your frame. Two prompts fight, and yours is the one the
player sees first.
⚠️ The mute button is the portal's, but the muting happens in your frame. A
cross-origin iframe has no muted property, so the SDK interposes a gain node
on your AudioContext. It works without you doing anything — but only because
the bridge is injected ahead of the loader, which is why step 2 above is not
merely tidy. Tick Sound in SquidHub → Game Manifest… or no button appears.
⚠️ Editing manifest.json alone registers a new version. It used not to —
the manifest is excluded from the build hash, so the build looked identical and
the edit was discarded. A version is now identified by its bytes and its
declaration, so a manifest-only change ships. The bytes are still deduped: the
two versions share one copy in R2.
To change display on a build that is already live, the publisher console's override is still the faster path — it needs no upload at all.
⚠️ Status
Verified end to end on 2026-09-04. A real Unity WebGL build completed the
portal handshake, received its session, read a save, and had commerce.purchase
correctly refused as an ungranted capability.
⚠️ Upload your build UNCOMPRESSED — Player Settings → Publishing Settings → Compression Format → Disabled. Cloudflare compresses at the edge and sets the header; a precompressed build cannot be served correctly and fails in the loader with a message naming the wrong cause.
Support
Contact your SquidHub publisher representative.
MIT licensed.
