Per-user state persistence model
Your saved AI campaign drafts, your generated report history, and your dialpad favorites all live on your Orbit account. This page explains why those surfaces moved out of browser storage, what each/me endpoint stores, and what to do if an older integration or browser profile of yours still keeps a local copy.
The problem with browser-bound state
These surfaces first shipped writing to the browser’slocalStorage. That made “draft saved” true only for this browser, on this profile, on this device:
- A draft saved on the office workstation did not exist on the laptop at home.
- Clearing site data, a private-browsing window, or a fresh browser profile erased the history.
- On a shared workstation, the next operator to sign in saw the previous operator’s drafts, or lost their own.
What the platform now persists per surface
Each surface has a dedicated/me endpoint, and each stores per signed-in user:
The drafts, reports, and recommendations entries are stored as keys in the preferences record that already lives on your user row. The keys are:
saved_ai_campaign_draftsgenerated_reportsrecommendations_tester_draftinbox_show_archived_tags(your Inbox archived-tags toggle)inbox_signature(your Inbox reply signature)
- No schema change was required. The preferences column already existed and already held
inbox_signatureandinbox_show_archived_tags, so each new surface shipped without a migration window and without touching unrelated data. - Writes never clobber each other. Every update merges exactly its own key into the record and leaves every sibling key untouched, so saving your report history cannot erase a draft, a signature, or a toggle.
- The pattern is a template. If you build tooling on the API that keeps per-operator state, treat these endpoints as the model: one namespaced key per surface, merged writes, and defensive reads.
The GET/PUT shape
GET /v1/me/campaign-drafts answers { "data": { "drafts": [...] } }, where each draft carries id, name, brief, draft (the generated blueprint, or null when saved before generation), channels, createdAt, and updatedAt. PUT sends the complete list, { "drafts": [...] }, and stores exactly what you send, newest first, capped at 50.
Generated reports mirror the shape: GET answers { "data": { "reports": [...] } } and PUT accepts { "reports": [...] }, each entry carrying id, name, type, generatedAt, and columns, never rows.
Both sides read defensively: entries with missing or drifted fields are skipped rather than treated as errors, so an older client and a newer server keep working against each other.
Lifting state you already saved in the browser
The platform will not read your browser’s storage for you; there is no server-side auto-migration. If your team built tooling or a custom build that persisted these surfaces locally, run a one-time lift-and-shift from the client: read the local key once on app start,PUT it up to the matching /me endpoint, then clear the local copy.
The local key names the platform surfaces wrote before account-bound storage:
The dashboard still uses these keys as a fast local cache it reconciles against the account on every load, with the account copy winning. If your own tooling wrote to them, treat them as a read-once input for the lift, not as an authoritative store.
Sync then clear
The sequence the dashboard itself runs at start, in full:- Read the local key once on app start.
- Fetch the account copy first; never
PUTbefore thatGETsettles. - Union by
id, server copy winning. - Clear the local copy only after the
PUTsucceeds; on any failure leave it in place and retry on the next start.
Cross-device behaviour after the lift
Once state is lifted, or simply once you use the surfaces normally (the dashboard syncs on every change), the account copy is what every session reads:- Sign in on a second machine and your saved drafts, your report history, and your pinned favorites are already there.
- The mobile dashboard reads the same account store, so the dialpad favorites on your phone are the same pins as on the web dialpad.
- Clearing browser data no longer erases history; it only drops the local cache the dashboard uses to paint instantly before the network answers.
PII: report history is metadata-only
A generated report’s rows can carry your customers’ phone numbers and email addresses. The/v1/me/generated-reports PUT contract therefore has no rows field at all: only id, name, type, generatedAt, and columns are accepted and stored. The dashboard strips rows before every write, and the API contract enforces the same rule, so report history persists per user without persisting customer PII. If you extend the pattern in your own tooling, keep that discipline: persist the pointer, not the payload.
Speed-dial favorites: a real table, not a preferences key
/v1/me/favorites is the one surface in this model that does not live in the preferences record. Favorites are rows: each pin records what it points at (a number or a contact) and a sort_order, which is what lets you reorder the pinned strip on the dialpad. The API caps how many favorites one user may pin and answers 409 FAVORITE_LIMIT_REACHED when the cap is reached, and a duplicate pin of the same target is rejected. Favorites give every operator a personal speed dial on the shared dialpad, on web and mobile alike.
The distinction is the design lesson: a preferences key suits a bounded blob that one surface reads and writes; a real table is the right shape the moment a per-user feature needs ordering, per-row operations, or per-row limits.
Extending the pattern in your own tooling
When your integration keeps per-operator state (a composer draft, a saved filter, a console layout), follow the same contract the platform uses:- Key state to the signed-in user, never to the browser alone; where you keep a local cache for instant paint, key it by user id and treat the account copy as authoritative.
- One namespaced key per surface, merged on write, never a whole-record write-back.
- Decide what the payload may contain before you persist it, and strip contact-level data at the boundary the way report history does.
- Read defensively: skip unknown or drifted fields instead of erroring, so older clients survive newer payloads.
See also
- Developer console overview — the hub that groups the operator and developer surfaces built on the same API.
- Dashboard home — the landing surface whose per-user widgets read from the account store described here.