Skip to content

[spec] Cookie read/write and local storage APIs#14731

Draft
lukasmasuch wants to merge 2 commits intodevelopfrom
lukasmasuch/cookie-api-spec
Draft

[spec] Cookie read/write and local storage APIs#14731
lukasmasuch wants to merge 2 commits intodevelopfrom
lukasmasuch/cookie-api-spec

Conversation

@lukasmasuch
Copy link
Copy Markdown
Collaborator

@lukasmasuch lukasmasuch commented Apr 11, 2026

Describe your changes

Adds product and tech specs for two related client-side storage features:

  • Cookie Read/Write API (st.cookies) — First-class API for writing browser cookies, enabling persistent client-side storage that survives page refreshes (~4KB limit per cookie)
  • Local Storage API (st.local_storage) — First-class API for browser localStorage with auto JSON serialization (~5-10MB capacity)

Both specs include:

GitHub Issue Link (if applicable)

Testing Plan

  • Spec documents only — no code changes
Agent metrics
Type Name Count
skill finalizing-pr 1
subagent fixing-pr 1
subagent general-purpose 2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lukasmasuch lukasmasuch added the change:spec Issue contains a product or tech spec label Apr 11, 2026
Copilot AI review requested due to automatic review settings April 11, 2026 13:03
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 11, 2026

Spec PR Validation

✅ All checks passed!

@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Apr 11, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Comment on lines +101 to +102
**Note on `httponly`:** JavaScript cannot set HTTP-only cookies. If user specifies
`httponly=True`, log a warning. True HTTP-only cookies require ASGI middleware approach.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The note states that JavaScript cannot set HTTP-only cookies and suggests logging a warning, but the product spec example (line 144) shows httponly=True being used for session cookies. This creates a security vulnerability - users will think their session cookies are HTTP-only protected when they actually aren't.

The implementation should either:

  1. Raise an exception when httponly=True to prevent false security expectations, or
  2. Document clearly in the product spec that httponly=True cannot be used with this API and requires ASGI middleware instead
Suggested change
**Note on `httponly`:** JavaScript cannot set HTTP-only cookies. If user specifies
`httponly=True`, log a warning. True HTTP-only cookies require ASGI middleware approach.
**Note on `httponly`:** JavaScript cannot set HTTP-only cookies. If user specifies
`httponly=True`, raise a `ValueError` to prevent false security expectations. True HTTP-only cookies require ASGI middleware approach.

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Changed from logging a warning to raising a ValueError to prevent false security expectations. Also added a note that true HTTP-only cookies require server-side mechanisms.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new product + tech specs under specs/ proposing two first-class client-side storage APIs (st.cookies and st.local_storage) to enable persistence across refreshes/sessions and cover common auth/preferences/state use cases.

Changes:

  • Add a product spec + tech spec for a Cookie Read/Write API (st.cookies).
  • Add a product spec + tech spec for a Local Storage API (st.local_storage) including sync/namespace strategy and proposed proto/frontend/backend architecture.
  • Document API options, recommended designs, examples, and security considerations for both features.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 13 comments.

File Description
specs/2026-03-15-local-storage-api/tech-spec.md Technical design for syncing localStorage to/from backend (proto, frontend handler, backend proxy, context caching, namespacing).
specs/2026-03-15-local-storage-api/product-spec.md Proposed public API for st.local_storage, behaviors (timing, serialization, limits), examples, and checklist.
specs/2026-03-13-cookie-read-write-api/tech-spec.md Technical design for cookie writes via ForwardMsg and backend proxy sketch; notes HttpOnly limitation.
specs/2026-03-13-cookie-read-write-api/product-spec.md Proposed public API for st.cookies, behaviors, examples, security notes, and checklist.

Comment on lines +167 to +173
### Widget Binding (Related Feature)

The existing query-params binding spec (2026-01-06) includes plans for `bind="local-storage"`:

```python
st.selectbox("Theme", ["light", "dark"], bind="local-storage", key="theme")
```
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The widget-binding example uses bind="local-storage" and claims the existing 2026-01-06 query-param binding spec already includes this plan. In that spec, the proposed future extension is "localstorage" (no hyphen) and it’s framed as a possible later extension, not a committed plan. Align the enum spelling with the existing spec (or update that spec too) and soften the wording here if it’s not decided yet.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Updated the spelling to "localstorage" to match the existing query-params binding spec, and softened the wording to indicate it's a possible future extension rather than a committed plan.

Comment on lines +238 to +242
- **Client-only:** Data never sent to server (unlike cookies)
- **Same-origin:** Browser enforces same-origin policy
- **User-controlled:** Users can clear via browser dev tools
- **No sensitive data:** Docs will warn against storing passwords, tokens (use `st.cookies`
with `httponly=True` instead)
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section says localStorage data is “never sent to server”, but the tech spec proposes sending a localStorage snapshot to the backend on session connect so the Python API can read values. Please clarify the data-flow accurately (e.g., not sent with HTTP requests, but synced over the Streamlit websocket). Also, the guidance to use st.cookies with httponly=True conflicts with the cookie tech spec note that JS can’t set HttpOnly cookies; update the recommendation accordingly.

Suggested change
- **Client-only:** Data never sent to server (unlike cookies)
- **Same-origin:** Browser enforces same-origin policy
- **User-controlled:** Users can clear via browser dev tools
- **No sensitive data:** Docs will warn against storing passwords, tokens (use `st.cookies`
with `httponly=True` instead)
- **Browser-managed storage:** Data is stored in the browser and is not automatically
attached to HTTP requests like cookies, but the current design syncs a snapshot to the
backend over the Streamlit WebSocket so the Python API can read it
- **Same-origin:** Browser enforces same-origin policy
- **User-controlled:** Users can clear via browser dev tools
- **No sensitive data:** Docs will warn against storing passwords or tokens in
localStorage; sensitive values should use server-managed auth/session mechanisms (for
example, server-set `HttpOnly` cookies), not client-managed storage

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Updated the security section to clarify that data is synced over the Streamlit WebSocket (not truly client-only), and removed the incorrect httponly=True recommendation. Now recommends server-set HttpOnly cookies via ASGI middleware for sensitive values.

Comment on lines +152 to +159
**Namespacing:** Keys are automatically namespaced per-app to prevent cross-app collisions:

```
st_local_storage_{app_id}_{key}
```

Where `app_id` is derived from the app's URL path. This prevents one app from reading/
overwriting another app's data on the same domain.
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key namespacing format here (st_local_storage_{app_id}_{key}) doesn’t match the tech spec’s proposed prefix (st_ls_{hash(page_url_path)}_{user_key}) or the TS snippet’s st_ls_${getAppId()}_. Please pick one canonical on-disk key format and use it consistently across product + tech specs (including examples).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Aligned the namespacing format to st_ls_{app_id}_{key} in both product and tech specs, with clarification that app_id is derived from a hash of the URL path.

Comment on lines +163 to +165
- Individual values: Soft limit ~1MB (warn if exceeded)
- Total storage: ~5-10MB (browser-dependent)
- Raises `StreamlitAPIException` on `QuotaExceededError`
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Size-limit/error behavior is inconsistent with the tech spec. Here it says the 1MB limit is a “soft limit” with a warning and that QuotaExceededError becomes a StreamlitAPIException, but the tech spec’s Python proxy raises immediately once the JSON value exceeds 1MB and the TS handler rethrows the DOMException. Please align the intended behavior (warn vs raise, and how frontend quota errors are surfaced to Python/users).

Suggested change
- Individual values: Soft limit ~1MB (warn if exceeded)
- Total storage: ~5-10MB (browser-dependent)
- Raises `StreamlitAPIException` on `QuotaExceededError`
- Individual values: Hard limit ~1MB per JSON-serialized value (writes raise immediately if exceeded)
- Total storage: ~5-10MB (browser-dependent)
- Browser quota failures surface as the underlying storage error (`QuotaExceededError` / `DOMException`), rather than being remapped to `StreamlitAPIException`

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Changed from soft limit (warn) to hard limit (raises immediately). Browser quota errors are now re-raised as-is rather than wrapped in StreamlitAPIException.

Comment on lines +255 to +256
| Item | Status |
|------------------------------|--------|
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec template convention uses the checklist table header “✅ or comment” (not “Status”). To match the repo’s spec templates, please update the checklist table header/format accordingly.

Suggested change
| Item | Status |
|------------------------------|--------|
| Item | ✅ or comment |
|------------------------------|---------------|

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Updated to | Item | ✅ or comment | to match the repo's spec template convention.

Comment on lines +106 to +115
### Behavior

**Timing:** Cookies are queued during script run and sent in the ForwardMsg. They become
readable on the **next** page load/rerun.

```python
st.cookies["draft"] = "text" # Queued
st.rerun() # Sent to browser
# Next run: st.cookies["draft"] == "text"
```
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section claims cookies become readable on the “next page load/rerun”, but st.context.cookies (which this API is based on) is defined as cookies sent in the initial request / WebSocket handshake, and that context is cached for the lifetime of the connection. That means cookies set via document.cookie won’t be visible to the backend on a rerun without a full page refresh/reconnect (or an explicit cookie snapshot sent from the frontend). Please correct the timing semantics and update the example accordingly.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Clarified that cookies become readable only after a full page load or browser reconnect (not just st.rerun()), since st.context.cookies reflects cookies from the initial HTTP request/WebSocket handshake.

Comment on lines +140 to +145
```python
session_id = st.cookies.get("session")
if not session_id:
session_id = secrets.token_urlsafe(32)
st.cookies.set("session", session_id, max_age=7*24*60*60, httponly=True)
```
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API surface and example suggest httponly=True is supported for st.cookies.set(...), but the tech spec explicitly notes JavaScript cannot set HttpOnly cookies (it can only be done server-side via response headers/middleware). This is a security-relevant mismatch: either remove httponly from this client-side API, make it a hard error, or clearly document that it’s ignored/unsupported and provide a separate server-side mechanism if needed.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Removed httponly=True from the example since JS cannot set HTTP-only cookies. Added a comment explaining that ASGI middleware is needed for truly HTTP-only session cookies.

Comment on lines +165 to +166
| Item | Status |
|------------------------------|--------|
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec template convention uses the checklist table header “✅ or comment” (not “Status”). To match the repo’s spec templates, please update the checklist table header/format accordingly.

Suggested change
| Item | Status |
|------------------------------|--------|
| Item | ✅ or comment |
|------------------------------|----------------|

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Updated to | Item | ✅ or comment | to match the repo's spec template convention.

Comment on lines +96 to +103
max_age: int | timedelta | None = None, # None = session cookie
expires: datetime | None = None,
path: str = "/",
domain: str | None = None,
secure: bool = True, # HTTPS-only by default
httponly: bool = False, # JS-accessible by default
samesite: Literal["strict", "lax", "none"] = "lax",
) -> None:
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parameter list includes httponly, but the tech spec notes HttpOnly cookies cannot be set from JavaScript. Keeping an httponly option on a client-side cookie-writing API is misleading unless it’s explicitly documented as unsupported/ignored or routed through a server-side response mechanism.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Removed the httponly parameter from st.cookies.set() entirely since JS cannot set HTTP-only cookies. Added a comment in the API section explaining this limitation and pointing to ASGI middleware for HTTP-only cookies.

Comment on lines +135 to +141
**Timing:** Same as `st.cookies` - values are queued during script run and sent to browser
via ForwardMsg. Readable on **next** rerun.

```python
st.local_storage["draft"] = "my text" # Queued
st.rerun() # Sent to browser
# Next run: st.local_storage["draft"] == "my text"
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This claims localStorage timing is “same as st.cookies” and that values are readable on the next rerun. Given that cookies from st.context.cookies are only from the initial request/handshake, and given the current tech-spec sketch doesn’t update the backend cache after writes, this timing statement is currently inconsistent with both designs. Please define the localStorage read-after-write semantics precisely (next rerun vs next full reload) and ensure the tech spec supports it.

Suggested change
**Timing:** Same as `st.cookies` - values are queued during script run and sent to browser
via ForwardMsg. Readable on **next** rerun.
```python
st.local_storage["draft"] = "my text" # Queued
st.rerun() # Sent to browser
# Next run: st.local_storage["draft"] == "my text"
**Timing:** Values written with `st.local_storage` are queued during the script run and
sent to the browser via `ForwardMsg` after the run completes. They are **not** readable
again in the same run, and they are **not guaranteed** to be readable on the next rerun
of the same live session. With the design proposed here, read-after-write becomes visible
only after a **full page reload / new browser connection**, when the backend rehydrates
`st.local_storage` from the browser's localStorage during session initialization.
```python
st.local_storage["draft"] = "my text" # Queued and sent to browser after this run
st.rerun() # Does not make the new value readable yet
# Same live session: st.local_storage.get("draft") may still be None
# After a full browser reload / reconnect:
# st.local_storage["draft"] == "my text"

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Updated the timing section to clarify that read-after-write becomes visible only after a full page reload / new browser connection, not on the next rerun. The design now includes optimistic cache updates for same-session reads.

@lukasmasuch lukasmasuch marked this pull request as draft April 11, 2026 13:09
- Remove httponly parameter from st.cookies API (JS cannot set HTTP-only cookies)
- Clarify timing semantics: values readable after page reload, not just rerun
- Align namespacing format between product and tech specs (st_ls_{app_id}_{key})
- Add optimistic cache updates to support read-after-write in same session
- Add set() method with expires_in parameter to tech spec
- Fix size limit behavior: hard limit 1MB, re-raise browser quota errors
- Fix widget binding spelling to match existing spec ("localstorage" not "local-storage")
- Update checklist table headers to match spec template convention

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lukasmasuch lukasmasuch added the ai-review If applied to PR or issue will run AI review workflow label Apr 11, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Apr 11, 2026
@github-actions
Copy link
Copy Markdown
Contributor

⚠️ AI Review Failed

The AI review job failed to complete. Please check the workflow run for details.

You can retry by adding the 'ai-review' label again or manually triggering the workflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:spec Issue contains a product or tech spec

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants