The AI governance world split into two incompatible blocs this week: WAICO in Shanghai, the U.S.-led Pax Silica in the West. The strategic takeaway was "pick a lane or build for both." This is the engineering companion: how you build for both without forking your codebase in half.
If you read one line: resolve the user's region once, turn it into a policy object, and pass that object — not the region — through every model call, storage write, and moderation check. That single seam is the difference between a one-row change and a rewrite when the rules diverge.
The trap: region checks scattered inline#
Here's the pattern that feels fine on day one and becomes a liability by month six:
// DON'T: region logic smeared across the codebase
async function summarize(text, user) {
const model = user.region === "cn"
? await kimiClient.complete(text) // one place...
: await openaiClient.complete(text);
// ...and thirty other places check user.region too
}
Every one of those inline checks is a place you'll have to find and change when a third market shows up, or when one bloc adds a data-residency rule. You will miss one. The one you miss is the compliance incident.
The fix: one policy resolver#
Model the variation, not the region. Everything that differs by market goes into one plain object:
// policy.js — the single source of truth for what varies by market
const POLICIES = {
waico: { modelProvider: "kimi", dataRegion: "ap-shanghai", contentRuleset: "cn-2026" },
pax_silica: { modelProvider: "openai", dataRegion: "us-east-1", contentRuleset: "us-default" },
};
const REGION_TO_BLOC = { cn: "waico", ru: "waico", br: "waico", us: "pax_silica", de: "pax_silica" };
export function getPolicy(region) {
const bloc = REGION_TO_BLOC[region] ?? "pax_silica"; // safe default
return POLICIES[bloc];
}
Resolve the region once, as early as you can — at the edge, or on the first authenticated request — and persist it on the session:
// middleware — runs once per request, before any AI call
app.use((req, res, next) => {
const region = req.user?.region ?? geoFromRequest(req); // explicit setting wins over geo
req.policy = getPolicy(region);
next();
});
Everything downstream reads the policy, never the region#
Now the seam pays off. A single client factory picks the model:
// model.js — selected by policy, not by an inline if
const CLIENTS = { kimi: kimiClient, openai: openaiClient };
export function modelFor(policy) {
return CLIENTS[policy.modelProvider];
}
async function summarize(text, policy) {
return modelFor(policy).complete(text); // no region check in sight
}
Storage routes by policy.dataRegion, so a WAICO-bloc user's data physically cannot land in a Pax-Silica region:
function bucketFor(policy) {
return storage.bucket(`app-data-${policy.dataRegion}`);
}
And moderation keys off policy.contentRuleset, so each market gets the rules it's legally held to — without a second moderation service:
await moderate(output, { ruleset: policy.contentRuleset });
The seam doesn't make your storage compliant or your models legal. It guarantees you never accidentally cross a bloc boundary — which is where the real incidents come from.
Why building it today is the whole point#
The seam is cheap when your codebase is small and brutal when it's large. Build it even if every region currently maps to the same policy — an all-identical POLICIES table costs nothing and turns the eventual fork into a one-row edit instead of a scavenger hunt. Adding a third market later becomes: one new row in POLICIES, one new region mapping, and — if it needs a different model — one new client adapter. That's it.
The convenient world of one model, one bucket, one rulebook ended in Shanghai this week. The seam is how you keep shipping one codebase anyway.



