Question and Use Cases
A multi-tenant project management web app supports both server-side rendering and offline editing. It has four kinds of state: the server must recognize the login session across tabs; theme and language preferences should survive a later visit; a multi-step form draft belongs only to the current tab and may disappear when that tab closes; and up to 10,000 structured task records with image attachments must support offline queries, edits, and synchronization after reconnecting.
Assign this data to cookies, localStorage, sessionStorage, and IndexedDB. Explain how the design handles XSS and CSRF, origin isolation, a consistent first SSR render, concurrent tabs, quota and eviction, database version upgrades, logout and local cleanup, and offline synchronization.
The 10,000-record figure is a scenario constraint that forces discussion of structured queries, asynchronous access, and a sync protocol. It is not a browser capacity guarantee. The core skill is making frontend persistence decisions from Web platform semantics, so the category is frontend.
What the Interviewer Is Evaluating
First, does the candidate ask who reads the data, how long it lives, what shape it has, what a disclosure would cost, and which system is authoritative before selecting an API? A memorized capacity table cannot explain SSR, authentication, or offline consistency.
Second, can the candidate separate persistence from security? Persistence does not make localStorage suitable for a session identifier. Same-origin scripts can read and change it, so an XSS payload can steal or tamper with its data. An HttpOnly cookie prevents JavaScript from reading the session identifier, but it does not stop code that is already running in the page from issuing authenticated requests.
Third, does the candidate understand both sides of cookie behavior? The browser sends matching cookies with requests, which helps server rendering and authentication. The same automatic sending requires CSRF defenses. SameSite is one defense, not a replacement for origin checks, CSRF tokens, or reauthentication for sensitive actions.
Fourth, can the candidate treat IndexedDB as a fallible local replica? It supports asynchronous structured objects, indexes, and blobs, but it does not synchronize with a server. The application owns quota failures, eviction, cleanup, blocked upgrades, and concurrency conflicts.
Fifth, can the candidate provide an executable failure matrix instead of treating “it survives refresh” as the only test?
Clarifying Questions Before Answering
- Which values does the server need for the first render? If SSR must emit HTML for the correct login state, language, or theme, those values need a server-readable source, or the product must accept a change after client mounting.
- What authentication model is in use? This answer assumes server-side sessions. A pure API token design also needs explicit issuance, rotation, revocation, and cross-site request rules.
- Should the draft really be tab-scoped? If users expect recovery after closing the tab or continuation on another device,
sessionStorageno longer meets the requirement; use IndexedDB or the server. - Does offline data contain sensitive information? Shared devices, XSS, browser profiles, and local backups change what may be written to disk.
- How are 10,000 records queried and updated? Queries by project, update time, or sync status make indexes and transactions more important than simple key-value access.
- Is the server the final source of truth? This scenario assumes an authoritative server and a rebuildable local copy. Irreplaceable offline-created data needs stronger persistence, export, and conflict controls.
- What counts as a conflict? Concurrent device edits may use version rejection, field merging, or a business priority. The storage API cannot choose that policy for the product.
- Must logout remove every tenant's data? When multiple accounts share a browser, cleanup needs user and tenant namespaces so the next user cannot see the previous user's replica.
30-Second Answer Framework
“I map data by reader, lifetime, shape, sensitivity, and source of truth. Authentication uses a server session and a __Host- cookie with Secure, HttpOnly, and an appropriate SameSite value, plus CSRF defenses. Small non-sensitive preferences use localStorage, with a server-readable initial value when SSR needs one. A disposable single-tab draft uses sessionStorage. Structured records and images use IndexedDB with indexes, transactions, a versioned schema, and an outbox. The server remains authoritative; sync uses versions and idempotency keys and resolves conflicts explicitly. Treat local storage as clearable and quota-fallible, then test XSS, CSRF, multiple tabs, eviction, blocked upgrades, reconnects, and logout cleanup.”
Step-by-Step Deep Dive
Step 1: Build the decision matrix with five questions
For each value, ask five questions: is the reader the server, the current tab, or every same-origin page; is the lifetime one render, one tab, one browser session, or multiple sessions; is the data a small string, structured objects, or blobs; how damaging are disclosure and tampering; and is the server or local device authoritative?
That produces the initial allocation for this scenario:
Login session -> Server session + random identifier in an HttpOnly cookie
Theme and language -> localStorage; add a server-readable initial value when SSR requires it
Single-tab draft -> sessionStorage
Offline data/images -> IndexedDB + an application-level synchronization protocolThis is not a ranking by capacity. A cookie's defining capability here is reaching the server with requests. The defining boundary of sessionStorage is the top-level browsing context. IndexedDB provides asynchronous transactions, structured objects, and indexes.
Step 2: Make the server authoritative for authentication
The browser stores only a high-entropy, short-lived session identifier. Real permissions, expiry, and revocation live on the server. Set Secure, HttpOnly, an appropriate SameSite value, and preferably use a __Host- cookie without Domain and with a root path. Logout, password changes, and risk events invalidate the server session; deleting a browser value alone is insufficient.
HttpOnly reduces direct script access to the session identifier, but same-origin XSS can still issue authenticated actions from the active page. Output encoding, CSP, and other XSS defenses remain necessary, and high-risk operations can require reauthentication. Because matching cookies are sent automatically, state-changing requests should also validate origin and use a CSRF token. SameSite should not be the only control.
Putting the session identifier in localStorage makes it readable to any same-origin script that successfully runs. Client-side encryption is not an automatic fix: if page JavaScript can obtain the decryption key or call the decryption path, same-origin XSS usually can as well.
Step 3: Use localStorage for small, non-sensitive preferences
localStorage is origin-scoped, stores string keys and values, persists across browser sessions, and exposes synchronous operations. Small preferences such as theme, language, or table density fit. They are not permanent: clearing site data, ending a private browsing session, or browser policy can remove them.
The server cannot directly read localStorage. If the first SSR frame must use the correct language or theme, synchronize a validated preference to a server-readable cookie or user profile and define which copy wins. If the value is client-only, emit a stable default and switch after mounting so server HTML and the first client render do not disagree.
Avoid storing a large record array in localStorage and repeatedly parsing the entire JSON value. Synchronous serialization and main-thread access grow with the dataset, while the model lacks IndexedDB transactions and indexes.
Step 4: Put only disposable tab state in sessionStorage
sessionStorage is partitioned by origin and top-level browsing context. It survives reloads in the same tab and is cleared when the tab or window closes. That makes it appropriate for the current tab's step number and disposable draft, but not for a cross-tab cart, long-term recovery, or cross-device state.
The opener boundary is easy to miss. A newly opened page may initially receive a copy of its opener's sessionStorage; the two copies then change independently. If the draft must never be copied, remove the opener relationship or include a per-tab instance ID in the draft and validate it during recovery.
Same-origin script can also read and write sessionStorage. “Cleared on close” is not a reason to store a long-lived credential. Before persisting a draft, exclude fields such as passwords, payment data, or medical information that should not be written to disk.
Step 5: Build a rebuildable offline replica in IndexedDB
IndexedDB offers asynchronous requests, transactions, object stores, keys, indexes, and blob storage. It fits this scenario's structured records and images. Partition data by tenantId + userId, create object stores for tasks, attachments, and an outbox, and add only the indexes required by real queries such as project, update time, and sync state.
Use an explicit database version and incremental migrations. Opening a new version can be blocked by connections from old tabs. Those tabs should listen for versionchange, close their old connections, and ask the user to refresh. The new page should handle blocked rather than hanging forever during startup.
IndexedDB is a local database, not a synchronization service. A successful write means the local transaction committed. The application still records local and server versions, an operation ID, and sync state, and makes retries idempotent.
Step 6: Design offline synchronization and cross-tab conflicts explicitly
Commit each offline business change and its outbox entry in one IndexedDB transaction. After reconnecting, a worker uploads by operation ID. The server deduplicates with an idempotency key and compares the record version. Only after acknowledgement should another transaction update the server version and remove the outbox item. This avoids changing the record while losing the pending operation.
Conflict policy comes from the business. A low-risk preference might use last writer wins; task status can reject a version mismatch and ask the user to merge; financial or permission changes may forbid offline commits. A storage event or BroadcastChannel can tell other tabs to reload data, but a notification is not a lock and cannot replace IndexedDB transactions or server version checks. The storage event does not fire in the document that performed the write.
Step 7: Treat quota, eviction, and cleanup as normal failures
Browser quotas vary by browser, device, and mode. IndexedDB commonly uses best-effort storage, which a user can clear and a browser can evict under storage pressure. Writes can also fail for lack of quota. Use storage estimates to observe usage, request persistence cautiously for irreplaceable data, and always handle transaction and quota failures.
The policy should include attachment limits, least-recently-used cleanup, compression or deletion of server-confirmed old versions, and a recoverable “local space is full” state. On logout, revoke the server session first, then close database connections and remove that user and tenant's IndexedDB data, preferences, and drafts. Notify other tabs as well. Local deletion cannot substitute for server revocation.
Step 8: Verify boundaries with a failure matrix
Test the first SSR render and hydration; refresh, duplicate, newly open, and close tabs; logout from another tab; script-readable scope under XSS; cross-site state-changing requests; private browsing; cleared site data; quota exhaustion; an old tab blocking a database upgrade; two tabs editing concurrently; offline retries, duplicate and out-of-order responses, and conflicts; and switching tenants without exposing old data.
Passing means more than “the data remains.” Authentication is revocable, sensitive values are not exposed to JavaScript, drafts obey the tab boundary, an offline write cannot lose its outbox entry, duplicate sync does not duplicate business effects, upgrades recover, quota failures have a fallback, and the server can rebuild the local replica.
High-Quality Sample Answer
“I start with the reader, lifetime, data model, trust boundary, and source of truth rather than capacity. The server and every tab need the login session, so the real session lives on the server and the browser holds a __Host- cookie with Secure, HttpOnly, and an appropriate SameSite value. That reduces JavaScript token access, but XSS can still act as the user, and automatic cookie sending still requires a CSRF token, origin checks, and reauthentication for sensitive actions.
Theme and language are small non-sensitive preferences, so they use localStorage. If SSR needs them on the first frame, I synchronize a validated server-readable preference and define the authoritative copy. The current tab's disposable form draft uses sessionStorage: reload restores it and close removes it. I also account for a new page initially copying the opener's value.
The 10,000 tasks and images use IndexedDB. The database is partitioned by user and tenant and uses a versioned schema, indexes, and transactions. Every business edit and outbox entry commit together. On reconnect, the client uploads with an idempotent operation ID, and the server compares record versions before acknowledging or returning a conflict. Cross-tab notifications only cause a reload; local transactions and server versions provide correctness.
I treat all local storage as clearable, quota-fallible, and available to same-origin script. I handle quota errors, clean rebuildable attachments, close old connections during upgrades, and revoke the server session before clearing the user's local namespace on logout. My test matrix covers SSR and hydration, tab copies, XSS, CSRF, quota, eviction, blocked upgrades, concurrent editing, offline retries, and tenant switching.”
Common Mistakes
- Choosing mechanically from a capacity table → misses server readers, tab boundaries, and authority → use the five-dimensional matrix first.
- Putting the session identifier in
localStorage→ same-origin XSS can read and exfiltrate it → use a protected cookie backed by a server session and continue preventing XSS. - Assuming
HttpOnlyeliminates XSS → malicious script can still perform authenticated actions → separate token-theft defense from action defense. - Using only
SameSitefor CSRF → browser policies, request types, and business flows still have boundaries → combine tokens, origin checks, and reauthentication. - Making the server depend on
localStorage→ SSR cannot read browser storage → provide a server-readable initial value or accept a post-mount change. - Assuming a new tab always has empty
sessionStorage→ an opener can provide the initial copy → remove the opener or add a tab instance ID. - Putting a large array in
localStorage→ requires synchronous parsing, whole-value rewrites, and has no indexed transactions → use IndexedDB. - Assuming IndexedDB synchronizes automatically → local commit and server acknowledgement are different events → implement an outbox, idempotency, versions, and conflict policy.
- Treating cross-tab notification as a distributed lock → messages can be delayed and cannot decide a server conflict → reload after notification and use transactions plus versions for correctness.
- Assuming local data is permanent → cleanup, eviction, private mode, and quotas can remove it → make it rebuildable and handle write failures.
- Clearing only the cookie on logout → IndexedDB and preferences can leak into the next account → revoke server-side, then clean every user and tenant local replica.
Follow-Up Questions and Responses
Follow-up 1: Can I encrypt a token and put it in localStorage?
If page JavaScript can obtain the key or call the decryption path, same-origin malicious script that successfully runs usually can do so too. That does not address session theft from XSS in this scenario. A stronger boundary is a server session with an HttpOnly cookie plus XSS prevention, CSRF protection, rotation, and revocation. Client-side encryption addresses a threat only when its key is outside the same attack surface.
Follow-up 2: What if a draft must survive closing the tab?
The lifetime requirement has changed, so sessionStorage no longer fits. A non-sensitive draft that needs only local recovery can use IndexedDB. A draft that must cross devices or cannot be lost should synchronize to the server. Either option needs a retention period, user and tenant isolation, and rules excluding sensitive fields from persistence.
Follow-up 3: What if two tabs edit the same offline task?
Store a server version and local revision on each record and use an IndexedDB transaction for each write. BroadcastChannel or a data-change notification tells other tabs to reload, but the server still performs a conditional update against the version. After a conflict, the business chooses field merging, a user prompt, or rejection. The last tab to receive a message cannot become the source of truth.
Follow-up 4: What if an old tab blocks the IndexedDB upgrade?
Old connections listen for versionchange, close, and prompt for refresh. The new page handles blocked with a recoverable state instead of waiting indefinitely. Migrations are incremental, idempotent, and tolerate partial historical data. Before release, keep an old-version tab connected and open the new version to verify closure, messaging, and migration.
Follow-up 5: How should the app degrade when browser storage is full?
Handle quota and transaction errors, stop caching new attachments first, and remove old server-confirmed attachments and versions that can be rebuilt. An unsynchronized outbox has higher priority than downloadable cache. If space is still insufficient, tell the user to reconnect and synchronize or free space; never report a silent save as successful.
Follow-up 6: What is the correct logout order?
First ask the server to revoke the current session so copied cookies and still-open tabs also lose access. Then notify other tabs, stop synchronization work, close IndexedDB connections, remove databases, drafts, and preferences for the user and tenant, and finally enter the signed-out UI. If the network request fails, do not present a local-only fake logout as confirmed; restrict further work and show that revocation is pending.