v0.37.0 · Unity 2022.3 LTS and Unity 6 · pre-release

Unity AWS Toolkit documentation.

Install the package, stand up a real AWS backend, call every service, and recognise the errors the toolkit actually throws.

Back to the Unity AWS Toolkit overview

Install & initialize

  1. Import the package (Package Manager → My Assets, or a UPM tarball), then open Tools → ShipIt → AWS Toolkit.
  2. Create the settings asset on the Setup tab (it lands at Assets/Resources/ShipIt/AwsSettings.asset).
  3. Paste Region, UserPoolId, AppClientId, IdentityPoolId and S3Bucket from your CloudFormation stack outputs.
  4. Initialize once at startup and sign in.
using ShipIt.Aws;

AwsClient.InitializeFromResources();
var aws = AwsClient.Instance;

// Optional: restore a session persisted by a previous run.
await aws.Auth.RestoreSessionAsync();

App client IDs and pool IDs are public identifiers — access is enforced by IAM roles and app-client configuration, never by secrecy. Long-lived IAM keys never belong in the asset or a build.

Runtime UI binds to the client whenever it initializes: if you create the client after the scene loads, AwsAuthPanel and AwsLeaderboardPanel still attach through AwsClient.OnInstanceChanged.

Guided setup

The Setup tab is a linear first run. Each step checks itself off:

  1. Create the settings asset — one button, created at the path runtime loads.
  2. Deploy the AWS backend — copy/reveal the CloudFormation template and open the console, or (with the AWS CLI installed) press Deploy from the editor: the stack deploys from inside Unity and Import stack outputs fills the settings asset.
  3. Paste the stack outputs — the five values are written straight into the asset.
  4. Verify the connection — Identity Pool credentials → DynamoDB ListTables → configuration doctor → live bucket-CORS check, logged in order.
  5. Create the game-system tables — leaderboards, saves, flags, events (TTL enabled), balances and ledger.
  6. Play the demo — builds the wired auth + leaderboard scene at Assets/Demos/AwsDemo.unity.

Try it with no AWS account

Tick Demo mode in step 1 and every call is served from an in-memory backend: the game tables are seeded with a small leaderboard, a double_xp flag and a demo wallet with ledger, and the toolkit's own condition rules run for real, so best-wins scoring and save concurrency still hold. Editor and development builds only; release players ignore it and the Doctor warns while it is on.

The other seven tabs remain for day-to-day work:

Settings

Edit the asset inline; the status strip lists blocking issues and warnings.

Connection

Credential fetch, signed DynamoDB ping, and the one-button live smoke suite.

Packs

Provision game tables and copy scoped player/admin IAM policies.

Browser

Read-only DynamoDB scans rendered as plain JSON.

Generator

Turn a sample item into a [Serializable] C# model — and a typed repository whose key signatures come from DescribeTable.

Doctor

Static configuration checks plus live bucket-CORS validation.

Diagnostics

The runtime log ring buffer, streamed from the player.

Smoke suite

Signs up a test user, exercises every service, deletes its own user.

Need a legacy import artifact? Tools → ShipIt → AWS Toolkit → Export .unitypackage stages a fresh-GUID copy (tests and package.json excluded; the documentation set — including the agent bundle — plus link.xml and the license notices included) and writes ShipIt-Aws-Toolkit-<version>.unitypackage to the project root. Use the UPM package or the exported artifact in a project — not both, or the assemblies collide.

Backend quickstart

Samples~/CloudFormation/shipit-aws-backend.yaml creates the User Pool and app client, the Identity Pool and both roles, the four game tables, and a content bucket with CORS. Upload it in CloudFormation → Create stack → With new resources.

ParameterDefaultWhen to change it
AllowedCorsOrigin*Set your game's https origin before shipping WebGL.
AllowClientScoreWritestrueSet false once scores go through the Lambda.
EnableTestAutoConfirmfalseSet true only to run the live smoke suite.
SignInWithUsernameLeave as Username for the smoke suite.
AllowUnauthenticatedIdentitiestrueSet false to require sign-in for credentials.

The stack's Outputs tab lists exactly the five values the settings asset needs — plus AccountId, which the Packs tab uses to stamp resource ARNs into IAM policies.

One-click deploy from the editor

If aws --version works on your machine and the active profile can create CloudFormation stacks and IAM roles, the Setup tab can do all of this: Deploy from the editor runs aws cloudformation deploy with the bundled template (region, CORS origin, smoke-test auto-confirm and server-authoritative scoring as fields), streams the output into the window log, and Import stack outputs writes the five values into AwsSettings. Credentials stay with the CLI (profiles, SSO, environment) — no admin keys ever enter the project, and the manual console route remains as the fallback.

Table and bucket names are fixed (the runtime defaults to them), so run one stack per account and region. Delete a stack before creating a second.

Verification order

Each check depends on the one above it. Stop at the first failure rather than reading past it.

WhereActionConfirms
Setup / ConnectionFetch Identity Pool credentialsPools exist and roles are attached
Setup / ConnectionPing DynamoDB (ListTables)SigV4 signing works end to end
Setup / DoctorRun configuration checksSettings are internally consistent
DoctorCheck bucket CORS (live)WebGL can read the bucket
ConnectionRun live smoke testEvery service, round-tripped and cleaned up

The smoke suite signs a user up and immediately signs in, so deploy with EnableTestAutoConfirm=true and SignInWith=Username while testing; turn auto-confirm off afterwards.

AI agents & the Unity CLI

The package ships an agent bundle so coding agents work from the correct patterns instead of guessing: Documentation~/agent/AGENTS.md (ground rules), five task skills under Documentation~/agent/skills/ (setup, auth, WebGL, data, troubleshooting), and Documentation~/llms.txt as the documentation index.

With Unity's Pipeline package installed (com.unity.pipeline, Unity 6+), the toolkit also registers eleven aws_* Unity CLI commands for agents and CI:

CommandWhat it does
aws_doctor · aws_verifyStatic checks, then a live verification pass (credentials, ListTables, doctor, bucket CORS).
aws_deploy · aws_import_outputsDeploy the bundled stack and write its outputs into AwsSettings.
aws_generate · aws_provision_tablesModel + typed repository from a live table; create any missing game tables.
aws_iam_policy · aws_cost_estimateScoped player/admin IAM JSON; monthly cost estimate from usage assumptions.
aws_create_settings · aws_release_check · aws_export_packageSettings-asset creation, the release gate and the store export.

Mutations are gated: without confirm=true they refuse, and dry_run previews the change. The command assembly is version-guarded — it compiles only when the Pipeline package is present, so Unity 2022.3 projects and runtime players are unaffected. The bundled skills can be installed into a project's .claude/skills/ folder from Tools → ShipIt → AWS Toolkit → Install Agent Skills.

Live commands use the credentials you already configured — Identity Pool via the settings asset for verification and generation, your AWS CLI profile for deploys. The toolkit never stores long-lived keys.

Auth — Cognito User Pools

Password, SRP, MFA continuation, Hosted UI social sign-in and account management, behind aws.Auth.

// Create + confirm
var signUp = await aws.Auth.SignUpAsync("ada", "Passw0rd!", new[] {
    new KeyValuePair<string, string>("email", "ada@example.com") });
if (!signUp.UserConfirmed)
    await aws.Auth.ConfirmSignUpAsync("ada", codeFromEmail);

// SRP sign-in — the password never leaves the device
var session = await aws.Auth.SignInWithSrpAsync("ada", "Passw0rd!");

// MFA continuation (SMS / TOTP / e-mail OTP)
try { await aws.Auth.SignInWithPasswordAsync("ada", "Passw0rd!"); }
catch (AwsAuthChallengeException mfa)
{
    var code = PromptForOtp(); // mfa.ChallengeName tells you which kind
    await aws.Auth.RespondToAuthChallengeAsync(mfa.ChallengeName, mfa.Session, code);
}

// Hosted UI (OAuth2 + PKCE — Google/Apple/Facebook)
var flow = aws.Auth.BeginHostedUiSignIn("mygame://auth");
Application.OpenURL(flow.AuthorizeUrl);
// … deep link arrives (see AwsDemoBootstrap) …
var social = await aws.Auth.CompleteHostedUiSignInAsync(flow, callbackUrl);

// Tokens, always fresh
string access = await aws.Auth.GetValidAccessTokenAsync();
string idToken = await aws.Auth.GetValidIdTokenAsync();

// Phone OTP / passwordless (CUSTOM_AUTH)
try { await aws.Auth.SignInWithCustomAuthAsync(phone); }
catch (AwsAuthChallengeException otp) // ChallengeName == "CUSTOM_CHALLENGE"
{
    await aws.Auth.RespondToCustomChallengeAsync(otp.Session, code);
}

// Account management
await aws.Auth.SendPasswordResetCodeAsync("ada");
await aws.Auth.ChangePasswordAsync("old", "new");
await aws.Auth.SignOutAsync(); // GlobalSignOut + local clear

Failures carry the Cognito error type in AwsAuthException.ServerCode (NotAuthorizedException, UserNotConfirmedException, …). Enable the flows you use on the app client: ALLOW_USER_PASSWORD_AUTH, ALLOW_USER_SRP_AUTH, ALLOW_REFRESH_TOKEN_AUTH and ALLOW_CUSTOM_AUTH for phone OTP. The OTP Lambdas ship as Samples~/CloudFormation/custom-auth-otp.yaml — a TTL code table, constant-time comparison, attempt burn-out, and a clearly marked delivery stub for your SMS provider.

Identity Pools — temporary credentials

Every signed service call uses short-lived STS credentials minted from your Identity Pool; the broker refreshes them automatically and caches the device identity.

var creds = await aws.Credentials.GetAsync();

// After account deletion or a device reset:
aws.Credentials.ForgetIdentity();

When the player is signed in, the credential request carries their ID token, so IAM can scope rows with ${cognito-identity.amazonaws.com:sub}. That is how cloud saves stay per-player without a server.

S3 storage

await aws.Storage.UploadAsync("screenshots/l3.png", bytes, "image/png");
var png  = await aws.Storage.DownloadAsync("screenshots/l3.png");
var meta = await aws.Storage.HeadAsync("screenshots/l3.png");
await aws.Storage.DeleteAsync("old.bin");

// Listing with pagination
var page = await aws.Storage.ListAsync(prefix: "saves/");
while (page.IsTruncated)
    page = await aws.Storage.ListAsync("saves/", page.NextContinuationToken);

// Presigned URLs — sharing and browser-direct uploads
var url = await aws.Storage.CreatePresignedGetUrlAsync("cut.mp4", TimeSpan.FromHours(2));

// Large files: chunked multipart with gap protection and auto-abort
var etag = await aws.Storage.UploadLargeAsync(
    "replays/match.bin", bigStream, contentType: "video/mp4");

// Progress (0..1) — pumped on the player loop, so WebGL works too
IProgress<float> progress = new Progress<float>(p => bar.value = p);
await aws.Storage.UploadAsync("clip.mp4", bytes, "video/mp4", progress);
var data = await aws.Storage.DownloadAsync("clip.mp4", progress);

// Resumable: reuse the same resumeId after a crash and it reconciles
// with ListParts and continues at the first missing part
var resumed = await aws.Storage.UploadLargeResumableAsync(
    "replays/match.bin", fileStream, "match-42");

// Give up on an interrupted upload and release its storage
await aws.Storage.AbortLargeUploadAsync("match-42");

// Asset helpers
Texture2D tex  = await aws.Storage.DownloadTextureAsync("img/logo.png");
AudioClip clip = await aws.Storage.DownloadAudioClipAsync("sfx/hit.ogg", AudioType.OGG);

WebGL needs a one-time bucket CORS rule allowing GET/PUT; the Doctor tab validates the live configuration and offers the sample rule.

DynamoDB data

Models are plain [Serializable] classes whose public fields are named exactly like the attributes. The marshaller handles numbers, binary, sets, lists and nested maps.

[Serializable]
public sealed class PlayerRow { public string UserId; public int Score; }

// CRUD with conditions
await aws.Data.PutItemAsync("players",
    new PlayerRow { UserId = "u1", Score = 10 },
    condition: new DynamoFilter().NotExists("UserId"));

var updated = await aws.Data.UpdateItemAsync<PlayerRow>("players",
    DynamoKey.Of("UserId", AttributeValue.S("u1")),
    new DynamoUpdate().Add("Score", AttributeValue.N(5)));

// Query a GSI, descending, reserved words escaped automatically
var top = await aws.Data.QueryAsync<PlayerRow>(new DynamoQueryRequest
{
    TableName = "players",
    IndexName = "ScoreIndex",
    KeyCondition = DynamoKeyCondition.HashEquals("Game", AttributeValue.S("g1"))
        .RangeGreaterThan("Score", AttributeValue.N(100)),
    ScanIndexForward = false,
    Limit = 50,
});

// Project only the attributes you need — payloads stay in DynamoDB
var slots = await aws.Data.QueryAsync<CloudSaveRow>(new DynamoQueryRequest
{
    TableName = "GameSaves",
    KeyCondition = DynamoKeyCondition.HashEquals("UserId", AttributeValue.S(userId)),
    Projection = DynamoProjection.Of("Slot", "Revision", "UpdatedAt", "DeviceLabel"),
});

Typed repositories

The Generator tab can emit a repository beside the model — key-typed get/delete, put with an optional condition, and a partition-key query. Key names and scalar types come from DescribeTable, so numeric keys become long and the generated methods build the right AttributeValue factories.

var saves = new SaveGameRepository(aws.Data);

var row  = await saves.GetAsync(userId, slot);
var page = await saves.QueryByUserIdAsync(userId, descending: true);
await saves.PutAsync(row, new DynamoFilter().NotExists("UpdatedAt"));

Also included: ScanAsync pagination, batch get/write with unprocessed-key retry, TransactWriteAsync, COUNT-only rank queries, and ExecuteStatementAsync for PartiQL.

Lambda functions

// Signed data-plane invoke (desktop / mobile)
var ack = await aws.Functions.InvokeAsync<AckPayload>("my-function", payloadJson);

// Response streaming — chunks arrive as they are produced
var full = await aws.Functions.InvokeStreamingAsync(
    "stream-fn", payloadJson,
    onChunk: chunk => AppendToUi(chunk));

// WebGL path: function URL (authType NONE — validate input server-side)
await aws.Functions.InvokeFunctionUrlAsync(url, payloadJson);

Function-level failures surface as AwsServiceException with the inline error type; use TryInvokeAsync to inspect a failed invocation without an exception.

Realtime WebSockets

Presence, chat, lobbies and live leaderboards over API Gateway WebSockets. Routes are yours — the client carries JSON strings, not a schema — and IAM-authorized APIs are reached with a presigned wss:// URL (browsers cannot set handshake headers; SigV4 query parameters are the workaround).

var url = await AwsRealtimeUrl.PresignAsync(
    aws.Settings, aws.Credentials, "abc123", "dev");

aws.Realtime.OnMessage += json => HandleServerEvent(json);
aws.Realtime.OnDisconnected += ex => Debug.LogWarning(ex.Message);

// API Gateway closes idle sockets after 10 minutes; ping to keep it open.
aws.Realtime.Options.HeartbeatJson = "{\"action\":\"ping\"}";

await aws.Realtime.ConnectAsync(url);
await aws.Realtime.SendAsync("{\"action\":\"subscribe\",\"channel\":\"lobby-1\"}");

While disconnected, sends queue up to MaxQueuedSends and flush after the next successful connect; reconnects use bounded exponential backoff. Editor, desktop and mobile use ClientWebSocket; WebGL uses the bundled browser bridge (needs a browser build to verify — tracked in the verification report).

Push registration

Re-engage players with mobile push via Amazon Pinpoint. The device token comes from the platform (Firebase on Android, APNs on iOS); the toolkit registers it as a Pinpoint endpoint with the player's temporary credentials and keeps it in sync.

var endpoint = new PinpointEndpoint
{
    Address = deviceToken,
    Channel = PinpointChannel.Gcm,
    UserId = userId,
    Attributes = { ["tier"] = new List<string> { "gold" } },
};

await aws.Pinpoint.RegisterEndpointAsync(endpointId, endpoint); // idempotent upsert
await aws.Pinpoint.SetOptOutAsync(endpointId, optOutAll: true); // "mute me"
await aws.Pinpoint.DeleteEndpointAsync(endpointId);             // on sign-out

Set PinpointApplicationId in the settings asset and grant the Identity Pool role mobiletargeting:UpdateEndpoint on the app. Channels: GCM, APNS, APNS_SANDBOX, ADM, BAIDU. WebGL has no device token, so push is a mobile/desktop feature.

Drop-in panels

Two panel kits ship and mirror each other's behaviour — pick by UI stack. Both bind to the client whenever it initializes (and rebind if it is replaced), so scene order doesn't matter.

  • uGUI — AwsAuthPanel and AwsLeaderboardPanel: wire the inspector fields and drop them in a scene.
  • UI Toolkit — AwsAuthPanelUIToolkit and AwsLeaderboardPanelUIToolkit with UXML/USS: add a UIDocument whose PanelSettings assigns a theme style sheet, assign the panel's UXML, add the controller — no inspector wiring.

The auth panel answers MFA and forced-password challenges through Confirm; the leaderboard waits for sign-in and refreshes automatically. Busy states dim the form, and errors surface as ServerCode: Message.

Game systems

Five drop-in systems on DynamoDB, provisioned in one click from Packs (or step 5 of the Setup wizard).

// Leaderboards — best-wins, with optional server-authoritative scoring
var r     = await aws.Game.Leaderboards.SubmitScoreViaLambdaAsync("global", userId, 9001);
var top   = await aws.Game.Leaderboards.GetTopAsync("global", 10);
long rank = await aws.Game.Leaderboards.GetRankForScoreAsync("global", 9001);

// Cloud saves with optimistic concurrency
var row  = await aws.Game.Saves.LoadAsync(userId, "slotA");
var save = await aws.Game.Saves.SaveAsync(userId, "slotA", json,
    expectedRevision: row?.Revision ?? null);
if (!save.Saved) { /* reload and merge */ }

// Feature flags — deterministic percentage rollout per player
if (await aws.Game.Flags.IsEnabledAsync("double_xp", userId)) { }
var cfg = aws.Game.Flags.GetPayload("double_xp");

// Analytics — buffered, batched by 25, TTL-retained
aws.Game.Analytics.Track(userId, "level_complete", "{\"level\":3}");
await aws.Game.Analytics.FlushAsync();

// Player economy — transactional balances + ledger, idempotent replays
var grant = await aws.Game.Economy.GrantAsync(userId, "gems", 100, idempotencyKey: "daily-2026-09-11");
var spend = await aws.Game.Economy.SpendAsync(userId, "gems", 25, idempotencyKey: "revive-7f3a");
long balance = await aws.Game.Economy.GetBalanceAsync(userId, "gems");
var recent   = await aws.Game.Economy.GetLedgerAsync(userId); // newest first
Analytics flush automatically on application pause and quit, so events tracked just before a suspend are not lost. You can still call FlushAsync() at checkpoints. Balances and ledger move in one DynamoDB transaction, and replayed idempotencyKeys report Duplicate instead of double-crediting — a server-authoritative game-grant-currency template ships in Packs.

Offline queue

Durable mutations for players who lose connectivity mid-session. The journal survives restarts and marshals payloads immediately, so later edits cannot leak in.

// While offline
aws.Offline.EnqueuePut("players", row);
aws.Offline.EnqueueDelete("players", key);

// Key-aware enqueues coalesce repeated edits to one entity
aws.Offline.EnqueuePut("GameSaves", slotRow, "UserId", "Slot");
aws.Offline.EnqueueDelete("players", key, coalesce: true);

// Strict order by default — stops on the first failure
var report = await aws.Offline.ReplayAsync(aws.Data);

// …or let independent tables skip-and-continue into a dead-letter list
var options = new OfflineReplayOptions
{
    Strategy = OfflineReplayStrategy.SkipFailedAndContinue,
};
options.TableStrategies["GameFlags"] = OfflineReplayStrategy.SkipFailedAndContinue;
var tolerant = await aws.Offline.ReplayAsync(aws.Data, options);

// Inspect or retry dead letters
aws.Offline.RetryFailed();
aws.Offline.ClearFailed();

Transport failures always stop a pass (the client is still offline); only permanent failures are dead-lettered, with the error text attached. The live queue — pending and failed entries with payloads — is visible while the game runs under AWS Toolkit → Diagnostics.

Drop-in UI panels

Two legacy uGUI panels ship in the runtime assembly. Wire the inspector fields, drop them in a scene, and they run:

AwsAuthPanel

Username / password / confirmation-code fields, sign-in, sign-up, confirm and Hosted UI buttons, status text. Challenges (MFA, forced password change) pause on the confirm button automatically.

AwsLeaderboardPanel

Top-N display plus score submission, optional serverAuthoritative routing through the Lambda. Waits for sign-in, then loads and refreshes automatically.

AwsDemoBootstrap

Initializes the client from Resources and completes Hosted UI deep links on mobile and desktop.

Both panels rebind when the client is created or replaced, so execution order no longer matters. Tools → ShipIt → Create AWS Demo Scene wires all three into a playable scene.

WebGL & platforms

Every call is plain HTTPS through UnityWebRequest: no native plugins, managed SHA-256/HMAC only, works everywhere Unity does.

SurfaceDesktop / consoleAndroid / iOSWebGL
Cognito User Poolsdirectdirectendpoints are CORS-enabled
Cognito Identity Poolsdirectdirectdirect
S3 objectsdirectdirectafter per-bucket CORS config
Lambda invokedata planedata planeFunction URLs
DynamoDB data planedirectdirectroute through a Function URL / API Gateway proxy
API Gateway WebSocketsClientWebSocketClientWebSocketbundled jslib (verify in a browser build)
Pinpoint push registrationdirectdirectn/a — browsers have no device token

S3 bucket CORS

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT", "HEAD"],
    "AllowedOrigins": ["https://your-game.example.com"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]

Lambda on WebGL

Direct lambda.<region>.amazonaws.com calls have no CORS headers. Create a Function URL: with authType NONE call InvokeFunctionUrlAsync(url, payload) and validate input inside the function; with authType AWS_IAM pass sign: true.

DynamoDB on WebGL

Route reads and writes through an API Gateway HTTP API or a thin Function URL proxy that accepts your JSON and performs the DynamoDB call server-side. Keep the proxy minimal and validate claims there.

IL2CPP & code stripping

link.xml preserves the runtime assembly. Your own model classes also need preservation under Medium/High stripping:

<assembly fullname="Assembly-CSharp">
  <namespace fullname="YourGame.Models" preserve="all"/>
</assembly>

IAM & security

  • No long-lived keys in clients. Every signed call uses temporary Identity Pool credentials.
  • Player rows are scoped to the caller with dynamodb:LeadingKeys on ${cognito-identity.amazonaws.com:sub} — cloud saves partition by identity.
  • Leaderboard writes cannot be scoped per player through IAM (only the partition key, the board). Best-wins conditions stop honest clients lowering a score, not a modified client. For production, deploy the game-submit-score Lambda, call SubmitScoreViaLambdaAsync, and generate the player policy with client score writes disabled.
  • App client secrets ship inside builds and are extractable. Prefer a secret-less public client; the Doctor flags a configured secret.
  • WebGL DynamoDB proxies must validate claims, because the data plane is architecturally closed to browsers.

The Packs tab generates player and admin IAM policies scoped to the four game tables. Attach the player policy to the Identity Pool's authenticated role; use the admin policy for one-time provisioning.

Verification & hardening

The package ships a published verification report: offline suite counts per release, the external reference vectors every protocol path is pinned to (AWS docs, RFC 3526/5054/7636, boto3, botocore), and the explicit list of what is not yet live-verified. SECURITY.md carries the full threat model and a pre-ship checklist.

Live verification is a repeatable pass rather than a vibe: the device matrix documents how to run the Connection-tab smoke suite per platform, and each run produces a Markdown artifact — platform, OS, device model, Unity/app versions, region and every step's result — ready to attach to the verification record. On phones and in browsers, the runtime AwsSmokeRunner component runs the same suite with an on-screen pass/fail overlay and writes the same artifact, so a device pass needs no debugger.

Cost awareness

The toolkit is a client library, so the bill is your AWS account's. A built-in estimator turns coarse usage assumptions into a monthly per-service breakdown, with editable rates because AWS list prices drift.

var estimate = AwsCostEstimator.Estimate(new AwsUsageEstimate
{
    MonthlyActiveUsers = 30_000,
    DailyActiveUsers = 10_000,
    DynamoWritesPerDauPerDay = 20,
    DynamoReadsPerDauPerDay = 100,
    DynamoStorageGb = 5,
    LambdaInvocationsPerDauPerDay = 10,
    S3StorageGb = 20,
    S3EgressGb = 5,
});

Debug.Log($"~${estimate.MonthlyTotalUsd:0.00}/month");

At 10,000 DAU that works out to roughly $128/month — and Cognito MAU (~$110) dominates, not the game data. The same estimator is inline on the Packs tab.

  • On-demand DynamoDB and TTL-retained analytics rows by default.
  • Projections and key-aware offline coalescing cut transferred bytes and writes.
  • Presigned URLs keep downloads direct to S3 — there is no proxy tier to pay for.

Troubleshooting

These are the strings the toolkit actually throws, so they are greppable in your logs.

What you seeAlmost always means
AwsSettings is invalid. Ensure Region, UserPoolId and AppClientId are set.A field is still blank. The Setup/Settings tab lists which.
Cognito did not return credentials.Identity Pool roles are not attached, or its authenticated provider does not point at this User Pool + app client.
NotAuthorizedException on sign-inThe auth flow is not enabled on the app client (ALLOW_USER_PASSWORD_AUTH / ALLOW_USER_SRP_AUTH).
InvalidParameterException on sign-upThe pool was deployed with SignInWith=Email; usernames must be e-mail addresses.
Smoke suite reports user not confirmedEnableTestAutoConfirm was false at deploy time.
AccessDenied from flags or analyticsThe authenticated role is missing those tables — re-copy the Player policy from the Packs tab.
UserPoolId must look like '<region>_<poolName>' for SRP.Paste the User Pool Id (us-east-1_AbC123), not the pool name or ARN.
S3Bucket is not configured on AwsSettings.Storage calls and the Doctor's CORS check need the bucket name from the stack outputs.
Browser blocks bucket reads in WebGLAllowedCorsOrigin does not match your page's origin. Confirm with Doctor → Check bucket CORS.
Stack ends in ROLLBACK_COMPLETE, a resource already existsA second stack in the same account and region: the bucket and four tables have fixed names. Update the existing stack, or delete it (empty the bucket first) and re-create.
403 with SignatureDoesNotMatch on a live callPath/signature disagreement class of bug — update to 0.17.0 or later, which pinned every signing path against botocore vectors.
If a signed call fails against real AWS but passes the offline suite, reproduce it in Connection → Run live smoke test first: it exercises every service with one log you can share.

Support

Email shipit.unity@gmail.com with your Unity version, target platform and the relevant log lines. The complete engineering changelog ships inside the package as CHANGELOG.md; release highlights are on the updates page.