Edit the asset inline; the status strip lists blocking issues and warnings.
Install & initialize
- Import the package (Package Manager → My Assets, or a UPM tarball), then open Tools → ShipIt → AWS Toolkit.
- Create the settings asset on the Setup tab (it lands at
Assets/Resources/ShipIt/AwsSettings.asset). - Paste
Region,UserPoolId,AppClientId,IdentityPoolIdandS3Bucketfrom your CloudFormation stack outputs. - 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.
AwsAuthPanel and AwsLeaderboardPanel still attach through AwsClient.OnInstanceChanged.Guided setup
The Setup tab is a linear first run. Each step checks itself off:
- Create the settings asset — one button, created at the path runtime loads.
- 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.
- Paste the stack outputs — the five values are written straight into the asset.
- Verify the connection — Identity Pool credentials → DynamoDB
ListTables→ configuration doctor → live bucket-CORS check, logged in order. - Create the game-system tables — leaderboards, saves, flags, events (TTL enabled), balances and ledger.
- 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:
Credential fetch, signed DynamoDB ping, and the one-button live smoke suite.
Provision game tables and copy scoped player/admin IAM policies.
Read-only DynamoDB scans rendered as plain JSON.
Turn a sample item into a [Serializable] C# model — and a typed repository whose key signatures come from DescribeTable.
Static configuration checks plus live bucket-CORS validation.
The runtime log ring buffer, streamed from the player.
Signs up a test user, exercises every service, deletes its own user.
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.
| Parameter | Default | When to change it |
|---|---|---|
AllowedCorsOrigin | * | Set your game's https origin before shipping WebGL. |
AllowClientScoreWrites | true | Set false once scores go through the Lambda. |
EnableTestAutoConfirm | false | Set true only to run the live smoke suite. |
SignInWith | Username | Leave as Username for the smoke suite. |
AllowUnauthenticatedIdentities | true | Set 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.
Verification order
Each check depends on the one above it. Stop at the first failure rather than reading past it.
| Where | Action | Confirms |
|---|---|---|
| Setup / Connection | Fetch Identity Pool credentials | Pools exist and roles are attached |
| Setup / Connection | Ping DynamoDB (ListTables) | SigV4 signing works end to end |
| Setup / Doctor | Run configuration checks | Settings are internally consistent |
| Doctor | Check bucket CORS (live) | WebGL can read the bucket |
| Connection | Run live smoke test | Every 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:
| Command | What it does |
|---|---|
aws_doctor · aws_verify | Static checks, then a live verification pass (credentials, ListTables, doctor, bucket CORS). |
aws_deploy · aws_import_outputs | Deploy the bundled stack and write its outputs into AwsSettings. |
aws_generate · aws_provision_tables | Model + typed repository from a live table; create any missing game tables. |
aws_iam_policy · aws_cost_estimate | Scoped player/admin IAM JSON; monthly cost estimate from usage assumptions. |
aws_create_settings · aws_release_check · aws_export_package | Settings-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.
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 —
AwsAuthPanelandAwsLeaderboardPanel: wire the inspector fields and drop them in a scene. - UI Toolkit —
AwsAuthPanelUIToolkitandAwsLeaderboardPanelUIToolkitwith UXML/USS: add aUIDocumentwhose 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
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:
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.
Top-N display plus score submission, optional serverAuthoritative routing through the Lambda. Waits for sign-in, then loads and refreshes automatically.
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.
| Surface | Desktop / console | Android / iOS | WebGL |
|---|---|---|---|
| Cognito User Pools | direct | direct | endpoints are CORS-enabled |
| Cognito Identity Pools | direct | direct | direct |
| S3 objects | direct | direct | after per-bucket CORS config |
| Lambda invoke | data plane | data plane | Function URLs |
| DynamoDB data plane | direct | direct | route through a Function URL / API Gateway proxy |
| API Gateway WebSockets | ClientWebSocket | ClientWebSocket | bundled jslib (verify in a browser build) |
| Pinpoint push registration | direct | direct | n/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:LeadingKeyson${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-scoreLambda, callSubmitScoreViaLambdaAsync, 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 see | Almost 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-in | The auth flow is not enabled on the app client (ALLOW_USER_PASSWORD_AUTH / ALLOW_USER_SRP_AUTH). |
InvalidParameterException on sign-up | The pool was deployed with SignInWith=Email; usernames must be e-mail addresses. |
Smoke suite reports user not confirmed | EnableTestAutoConfirm was false at deploy time. |
AccessDenied from flags or analytics | The 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 WebGL | AllowedCorsOrigin does not match your page's origin. Confirm with Doctor → Check bucket CORS. |
Stack ends in ROLLBACK_COMPLETE, a resource already exists | A 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 call | Path/signature disagreement class of bug — update to 0.17.0 or later, which pinned every signing path against botocore vectors. |
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.