DEV Community

Cover image for The Deployer agent and the multi-tenant backend
Sunitha Eswaraiah
Sunitha Eswaraiah

Posted on

The Deployer agent and the multi-tenant backend

Post 7 of 8 in the game-factory series.

The gate that says no

The Deployer is the last thing in the pipeline. Every other agent builds toward it: the Designer produces a spec, Image-Gen generates icons, Builder patches the React code, Tester plays the built game in a real browser. All of that work flows into a single question: does the Deployer run?

The answer isn't always yes. The Deployer reads the Tester's report before it touches anything. If the Tester flagged the build as broken — spin didn't work, win didn't register, login failed — the Deployer refuses to start. Not with an error. Just a refusal. The build exists on disk but it won't ship.

That refusal is the thing I'm most glad I built. Every automated pipeline I've seen either skips pre-deploy testing entirely or tests in a way that doesn't check whether the product actually works. The Tester runs a real browser, plays the game, and looks at screenshots to decide if it passed. The Deployer treats that judgment as a hard gate.

What follows, if the gate opens, is the part that involves AWS.

What it is

The Deployer takes a built React app and ships it to AWS using SAM — the Serverless Application Model, which is CloudFormation with a better syntax for serverless resources. The frontend ends up behind CloudFront, served from S3. The backend it points to is a serverless stack: API Gateway in front, Lambda functions handling game logic, DynamoDB storing player data and leaderboard entries.

It doesn't deploy silently. Before anything changes, it creates a changeset — CloudFormation's dry-run mechanism, which calculates what will be modified without actually modifying it — and shows it to me. If the changeset looks wrong, I can say no. This is the same "agent proposes, human approves" pattern that runs through the whole pipeline: the Designer waits for approval on the spec, the Builder shows its plan before touching code, the Deployer confirms before it ships.

The deploy has two halves. The backend goes through sam build and sam deploy — I show the changeset first, then run with the flag that skips the interactive confirmation, because the review already happened. The frontend is a separate step: aws s3 sync uploads the React build to the theme's S3 bucket, then a CloudFront invalidation busts the CDN cache so the new version is immediately live.

What it did

The obvious outcome is live games at real URLs. Two themed variants are deployed as of this writing: ancient-egypt-slots and norse-mythology-slots, both playable, both with functioning leaderboards and real DynamoDB tables behind them.

The less obvious outcome — and the one that took more work — is the backend architecture underneath both of them.

The original casino had a single-tenant backend. Every Lambda function, every DynamoDB table, every API Gateway route was built for one game. When I started generating themed variants, the straightforward path would have been to deploy a separate backend stack per theme: ancient-egypt-slots gets its own Lambdas, norse-mythology-slots gets its own Lambdas, every future theme gets another full stack.

That's expensive and it's unmanageable. Five themes means five stacks to maintain, five DynamoDB configurations to keep in sync, five deployments every time I fix a bug in the spin logic. The better structure was a shared backend with every read and write keyed by a theme_id.

So I forked the original casino backend into a multi-tenant version: one serverless stack — deployed to eu-west-1 — that serves every themed game at once. A new theme doesn't need its own backend. It registers as another tenant of the shared one.

What went wrong

Two categories of failure. They're quite different.

The multi-tenancy migration was a real migration. "Add a theme_id field" sounds like an afternoon. It's not, when the system you're adding it to was designed around one tenant and every path through it assumes that implicitly. I found cases where theme_id was being passed correctly to Lambda but wasn't making it into the DynamoDB query, so a spin in one game would sometimes read credit data from another. I found cases where the validation checked that the field was present but not whether it matched any known theme. Finding those cases meant writing tests that ran both themes simultaneously and checked for data bleed — not a standard integration test shape.

The migration also carried its own risk, separate from any theming work. DynamoDB doesn't let you alter an existing table's primary key, so I created new tables with theme_id as the partition key and migrated the application's queries to use them. On a development backend with no real player data, that was straightforward. In production with existing players, the same migration would mean careful data backfill and dual-read logic during the transition.

The lesson: making a system multi-tenant is a migration, not a configuration. Plan for it the way you'd plan for any schema change.

CloudFormation fails in its own language. The Deployer is the stage where the application code becomes irrelevant to the failure. A bug in the React build surfaces in the Tester, not here. If the Deployer fails, the failure is CloudFormation's, and CloudFormation doesn't speak Python or JavaScript.

I hit two of these. The first was a duplicate logical ID: two resources in the same SAM template with the same name. CloudFormation's error for this doesn't say "you have duplicate logical IDs." It surfaces as a schema validation error, and tracing it back to the specific duplicate took longer than it should have. The second was a ResourceExistenceCheck — a pre-flight check that validates whether a referenced resource exists before CloudFormation even creates a changeset. I had a reference to a resource the stack expected to already be there, and the check failed before a single resource was created or modified. The error arrived at changeset creation time rather than at deployment, which was useful, but still required reading CloudFormation event logs to understand.

Neither failure was in the application code. Both required CloudFormation documentation rather than anything else in the project's stack. The deploy step speaks a different dialect than every other step in the pipeline.

The good part: the test-report gate held every time. One build had a broken win state — reel symbols were rendering but win evaluation wasn't triggering. The Deployer refused it. I fixed the Builder's output, the Tester passed it, and the Deployer ran. The gate did exactly what it was supposed to do.

What to take from this

Three things.

Gate on real QA output, not on compilation. "The build compiled" is not evidence that the product works. A Tester that runs a browser, plays the actual game, and produces a structured report — which checks passed, which failed, screenshots for the failures — gives the Deployer something meaningful to block on. That check takes a few minutes. A broken game on a live URL is worse than a deploy that didn't run.

Key a shared backend by tenant ID — for low-stakes variants. For these themed games — same code, same owner, no compliance boundaries — one backend stack keyed by theme_id is simpler to operate than five isolated stacks. Every request carries the tenant key, every read and write is scoped to it, and adding a new variant is a registration step rather than a stack provisioning step. This wouldn't be the right call if tenants were separate customers with different security requirements. For variants of one game under one operator, it's the lighter path.

Budget separately for infrastructure failures. The Deployer is the first stage in this pipeline where the failure mode isn't in the Python or the React. CloudFormation errors don't look like application errors, don't point to line numbers in your code, and debugging them means learning CloudFormation's event model and pre-flight check behavior. That's a different skill than everything else in the pipeline requires. If you're building an automated deploy step, expect to spend time on this that feels orthogonal to the application work. It is orthogonal. It's also unavoidable.

The pipeline ends here. The chain runs end-to-end — one sentence in, one deployed game out, with a human approving three decisions along the way. Whether the result is good enough to call a product is a different question, and the answer is honest: not yet. The lessons from why are the subject of the final post.


Previous: the Tester agent. Next: the final post — observations and lessons.

Top comments (0)