orgId, projectId, and env — plus an API key. This page explains what those mean and what’s configured where.
The hierarchy
Organization
The top-level tenant. Holds billing, member access, audit logs, and one or more projects. Most teams need exactly one organization. You only need more if you have legally separate entities that must not share data.Project
A project is the unit of experimentation. It contains:- Its parameters, layers, and policies
- Its event definitions
- Its hashing configuration — most importantly the unit key and the bucket count
proj_marketplace). A B2B SaaS might have two — one keyed on userId for user-level experiments, one keyed on companyId for company-level experiments.
Environment
An environment is a runtime variant of the project. Production, staging, and development share the same parameters, layers, and policies — but each environment has its own parameter default overrides and its own API keys. What an environment lets you do:- Override a parameter’s default value per-environment. Set
feature.enabled = falseinstagingwhile keeping ittrueinproduction. The bundle forstagingships the override; the bundle forproductionships the canonical default. - Issue separate API keys so staging traffic and production traffic are clearly distinguished in dashboards and rate limits.
- Serve a separate bundle for each environment — the SDK fetches
bundle:{projectId}:{env}and only sees the values relevant for that environment.
- Policies are not environment-scoped. A policy is created in the project — it applies to every environment. Pausing a policy pauses it everywhere. If you need an experiment to run only in production (or only in staging), use targeting conditions keyed on a context field your SDK sends (e.g.
envordeployment_stage). - Layers, policies, allocations, conditions, and event definitions live at the project level. They’re the same across environments.
production, staging, and development out of the box. You can create more if you need (preview, qa, per-team environments).
API keys
API keys are scoped to a project and environment. Every key has the sametraffical_sk_... prefix — what differs is the key’s scopes, which determine what it can do and whether it is browser-safe:
| Scopes | Type | Where to use |
|---|---|---|
sdk:read, sdk:write | SDK key | All SDKs (@traffical/node, @traffical/js-client, @traffical/react, @traffical/svelte, React Native) and direct SDK API calls. Browser-safe — fetches the bundle and sends events but cannot modify configuration. |
mgmt:read, mgmt:write, or admin | Management key | Creating and modifying projects, layers, and policies. Must stay secret — never ship in client code. (The CLI normally uses device login instead.) |
The unit key
The unit key is the property of the user that determines bucketing. It’s a project-level setting. The most common choice isuserId:
companyIdfor B2B / multi-tenant SaaS where every user in an organization should see the same variantdeviceIdfor pre-login mobile experiencessessionIdfor cohort-based randomization- a custom key — anything stable the SDK can read out of
context
context argument you pass to getParams. If you set unitKey: userId, every call must include context.userId.
The unit key choice is hard to change later. If you start with
userId and switch to companyId, every assignment will reshuffle. Pick deliberately.Anonymous users
For browser SDKs, you often don’t have auserId yet — the user hasn’t logged in. The browser SDK auto-generates a UUID on first visit (the “stable ID”), stores it in localStorage with a cookie fallback, and uses it as the value of the project’s unit-key field. So a project keyed on userId ends up with the UUID standing in for userId until you call identify(realUserId).
Calling identify() overwrites the stable ID with the real user ID. From that point on, bucketing uses the real value — which produces different buckets from the anonymous phase. A user who saw the treatment variant while anonymous may see control after login, and there’s no built-in way to preserve the anonymous assignment across the login boundary (different unit-key value → different hash → different bucket).
For experiments where this matters, target logged-in users only (a policy condition on a context field your SDK sends, or simply running the experiment server-side after the user has been identified).
Bundle scope
Every config bundle is built for exactly one (project, environment) pair. The SDK fetches the bundle for the IDs you initialize it with, and every parameter, layer, and policy in scope is included. You don’t need to specify which experiments to load.env from production to staging swaps the bundle and the API key — your code doesn’t change.
A note on env
env is a free-form string. The default environments are production, staging, and development, but you can create environments with any name. Whatever string the SDK sends, the service looks up the matching bundle.
This is useful for ephemeral environments (per-PR previews, feature-branch deployments) — create the environment, scope the API key to it, and your team can experiment without touching production.
Next steps
Parameters
Typed values with defaults.
Layers
Mutual exclusivity and orthogonal bucketing.
Multi-tenant SaaS pattern
When to use a company-keyed project.