Skip to content

feat(extensions): add a Storage API for extensions#39171

Draft
michael-s-molina wants to merge 13 commits intoapache:masterfrom
michael-s-molina:extensions-storage-tiers-1-2
Draft

feat(extensions): add a Storage API for extensions#39171
michael-s-molina wants to merge 13 commits intoapache:masterfrom
michael-s-molina:extensions-storage-tiers-1-2

Conversation

@michael-s-molina
Copy link
Copy Markdown
Member

@michael-s-molina michael-s-molina commented Apr 7, 2026

Summary

Superset Extensions need a stable, safe, and well-scoped API to persist data. Without a first-class storage API, extensions are forced to roll their own storage — writing directly to localStorage with ad-hoc keys on the client, or inserting rows into Superset's internal key_value table on the server — with no consistent namespacing, no isolation from Superset's own data, and no defined cleanup path on uninstall. This PR adds a first-class storage API that implements Tier 1, Tier 2, and Tier 3 of the Extensions Storage Design Proposal.

Changes

  • Add managed storage APIs for extensions with automatic namespace isolation
  • Tier 1: Browser-based storage (ctx.storage.local, ctx.storage.session) with user isolation
  • Tier 2: Server-side cache (ctx.storage.ephemeral) with TTL support and Redis/Memcached compatibility
  • Tier 3: Database-backed persistent storage (ctx.storage.persistent) that survives server restarts and cache evictions
  • Storage is accessed via getContext() which is bound to each extension via Module Federation
  • Update webpack externals to support subpath imports like @apache-superset/core/extensions
  • Add new documentation page explaining how storage works

Tier 1: Local State (Browser Storage)

import { getContext } from '@apache-superset/core/extensions';

const ctx = getContext();

// Persists across browser sessions
await ctx.storage.local.set('sidebar_collapsed', true);
const isCollapsed = await ctx.storage.local.get('sidebar_collapsed');

// Cleared when tab closes
await ctx.storage.session.set('wizard_step', 3);

Tier 2: Ephemeral State (Server Cache)

import { getContext } from '@apache-superset/core/extensions';

const ctx = getContext();

// Store with default TTL (1 hour)
await ctx.storage.ephemeral.set('job_progress', { pct: 42, status: 'running' });

// Store with custom TTL (5 minutes)
await ctx.storage.ephemeral.set('quick_cache', data, { ttl: 300 });

// Retrieve
const progress = await ctx.storage.ephemeral.get('job_progress');
from superset_core.extensions.context import get_context

ctx = get_context()

# Store job progress
ctx.storage.ephemeral.set('job_progress', {'pct': 42, 'status': 'running'}, ttl=3600)

# Retrieve
progress = ctx.storage.ephemeral.get('job_progress')

Tier 3: Persistent State (Database)

Database-backed storage that survives server restarts, cache evictions, and browser clears. Use for any data that must not be lost.

import { getContext } from '@apache-superset/core/extensions';

const ctx = getContext();

// Store user preferences
await ctx.storage.persistent.set('preferences', { theme: 'dark', locale: 'en' });

// Retrieve
const prefs = await ctx.storage.persistent.get('preferences');

// Remove
await ctx.storage.persistent.remove('preferences');
from superset_core.extensions.context import get_context

ctx = get_context()

# Store user preferences
ctx.storage.persistent.set('preferences', {'theme': 'dark', 'locale': 'en'})

# Retrieve
prefs = ctx.storage.persistent.get('preferences')

# Remove
ctx.storage.persistent.remove('preferences')

Shared State (Cross-User)

All storage operations are user-scoped by default. Use the .shared accessor to read/write state visible to all users:

const ctx = getContext();

await ctx.storage.local.shared.set('device_id', 'abc-123');
await ctx.storage.ephemeral.shared.set('shared_result', { data: [1, 2, 3] });
await ctx.storage.persistent.shared.set('global_config', { version: 2 });

API Endpoints (Tier 2 & Tier 3)

All endpoints follow the pattern /api/v1/extensions/{publisher}/{name}/storage/{tier}/{key}. Append ?shared=true to any endpoint to operate on shared state visible to all users instead of the default user-scoped state.

Method Endpoint
GET /api/v1/extensions/{publisher}/{name}/storage/ephemeral/{key}
PUT /api/v1/extensions/{publisher}/{name}/storage/ephemeral/{key}
DELETE /api/v1/extensions/{publisher}/{name}/storage/ephemeral/{key}
GET /api/v1/extensions/{publisher}/{name}/storage/persistent/{key}
PUT /api/v1/extensions/{publisher}/{name}/storage/persistent/{key}
DELETE /api/v1/extensions/{publisher}/{name}/storage/persistent/{key}

Test plan

  • Verify ctx.storage.local persists data across page reloads in browser localStorage
  • Verify ctx.storage.session clears when browser tab is closed
  • Verify ctx.storage.ephemeral stores/retrieves data via the backend API
  • Verify ctx.storage.persistent stores/retrieves data and survives server restart
  • Verify storage is isolated per extension (different extensions using same key don't collide)
  • Verify user-scoped storage is isolated per user
  • Verify shared storage is accessible across users

🤖 Generated with Claude Code

@github-actions github-actions Bot added api Related to the REST API doc Namespace | Anything related to documentation dependencies:npm packages labels Apr 7, 2026
@dosubot dosubot Bot added change:backend Requires changing the backend change:frontend Requires changing the frontend doc:developer Developer documentation labels Apr 7, 2026
@michael-s-molina michael-s-molina marked this pull request as draft April 7, 2026 19:22
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 7, 2026

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 6748257
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/69d661901f68b30008647843
😎 Deploy Preview https://deploy-preview-39171--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 7, 2026

Codecov Report

❌ Patch coverage is 34.40454% with 347 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.42%. Comparing base (e9911fb) to head (f08bcd4).
⚠️ Report is 37 commits behind head on master.

Files with missing lines Patch % Lines
superset/extensions/storage/api.py 0.00% 131 Missing ⚠️
...uperset/extensions/storage/persistent_state_dao.py 32.18% 59 Missing ⚠️
superset-frontend/src/core/storage/localState.ts 11.42% 31 Missing ⚠️
superset/extensions/storage/ephemeral_state.py 45.45% 30 Missing ⚠️
...perset/extensions/storage/persistent_state_impl.py 48.21% 29 Missing ⚠️
...perset-frontend/src/core/storage/ephemeralState.ts 14.81% 23 Missing ⚠️
...erset-frontend/src/core/storage/persistentState.ts 19.23% 21 Missing ⚠️
superset/extensions/context.py 55.26% 17 Missing ⚠️
...perset-frontend/src/extensions/ExtensionsLoader.ts 0.00% 3 Missing ⚠️
superset/initialization/__init__.py 0.00% 2 Missing ⚠️
... and 1 more

❌ Your project check has failed because the head coverage (99.81%) is below the target coverage (100.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #39171      +/-   ##
==========================================
+ Coverage   64.40%   64.42%   +0.01%     
==========================================
  Files        2553     2568      +15     
  Lines      132558   133710    +1152     
  Branches    30746    30797      +51     
==========================================
+ Hits        85377    86143     +766     
- Misses      45695    46081     +386     
  Partials     1486     1486              
Flag Coverage Δ
hive 39.96% <36.70%> (+<0.01%) ⬆️
javascript 66.04% <25.00%> (-0.05%) ⬇️
mysql 60.44% <36.70%> (-0.16%) ⬇️
postgres 60.52% <36.70%> (-0.16%) ⬇️
presto 41.74% <36.70%> (-0.02%) ⬇️
python 62.10% <36.70%> (-0.17%) ⬇️
sqlite 60.15% <36.70%> (-0.16%) ⬇️
superset-extensions-cli 90.82% <ø> (?)
unit 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread superset/utils/cache_manager.py Outdated
Comment thread superset-frontend/src/core/storage/ephemeralState.ts Outdated
Comment thread superset-frontend/packages/superset-core/src/storage/localState.ts Outdated
Comment thread superset-frontend/packages/superset-core/src/storage/localState.ts Outdated
Comment thread superset/extensions/storage/ephemeral_state.py Outdated
Comment thread superset-frontend/src/extensions/ExtensionsStartup.tsx Outdated
Comment thread superset/extensions/storage/api.py Outdated
Comment thread superset/extensions/storage/api.py Outdated
Comment thread superset/extensions/storage/api.py Outdated
Comment thread superset/extensions/storage/api.py Outdated
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 8, 2026

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 93e3172
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/69d94ebcaffe9e00087f0b3f
😎 Deploy Preview https://deploy-preview-39171--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown
Member

@villebro villebro left a comment

Choose a reason for hiding this comment

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

First pass comments

Comment thread docs/developer_docs/extensions/storage.md Outdated
Comment thread docs/developer_docs/extensions/storage.md
Comment thread docs/developer_docs/extensions/storage.md
/**
* Default TTL in seconds (1 hour).
*/
export const DEFAULT_TTL = 3600;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

again, for better or for worse, but these have typically been defined in config.py to make them configurable. I would almost prefer to not have a default value at all, and make TTL a required parameter, than having this as a frontend defined fixed constant.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point. Addressed in d407804

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Let's:

  • Make ttl a required client parameter
  • Change CACHE_DEFAULT_TIMEOUT to MAX_TIMEOUT = 7 days to give admins more control.

Comment thread superset/extensions/storage/__init__.py Outdated
Comment thread superset/extensions/storage/api.py
Comment thread superset/extensions/storage/api.py Outdated
rusackas pushed a commit that referenced this pull request Apr 9, 2026
Builds on top of Michael Molina's Tier 1+2 storage PR (#39171) to add
database-backed persistent state for extensions.

Backend:
- ExtensionStorage model + migration (extension_storage table)
- ExtensionStorageDAO with upsert/get/delete across user/shared scopes
- PersistentStateImpl injected into superset_core.extensions.storage.persistent_state
- ctx.storage.persistent property added to ExtensionStorage in context.py
- REST endpoints GET/PUT/DELETE /api/v1/extensions/storage/persistent/<id>/<key>
- Fernet encryption support via EXTENSION_STORAGE_ENCRYPTION_KEYS config
- Use @transaction() decorator for DB write operations

Frontend:
- StorageTier declaration for persistent state in superset-core/src/storage
- persistentState exported from storage module index
- createPersistentState() factory in superset-frontend/src/core/storage
- persistent tier wired into ExtensionContext alongside local/session/ephemeral
- ExtensionStorage interface updated with persistent field

Docs:
- Added Tier 3 row to storage tier table
- Added Tier 3: Persistent State section (matching Tier 2 format)
- Frontend + backend + shared usage examples
- Configuration section for EXTENSION_STORAGE_ENCRYPTION_KEYS

Build/config:
- Added python_version = "3.10" to mypy config (needed for match statement)
- Added language_version: python3.10 to pre-commit mypy hook
- Rebuilt superset-core lib to include storage module and persistentState

Tests:
- Unit tests for PersistentStateImpl covering get/set/remove, shared scope,
  and error context guards

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added the risk:db-migration PRs that require a DB migration label Apr 10, 2026
@michael-s-molina michael-s-molina changed the title feat(extensions): add Tier 1 and Tier 2 storage APIs feat(extensions): add a Storage API for extensions Apr 10, 2026
michael-s-molina and others added 11 commits April 10, 2026 16:22
Implement managed storage APIs for extensions with automatic namespace
isolation. Storage is automatically bound to extensions before module
execution, ensuring data privacy between extensions.

- Tier 1 (localState/sessionState): Browser-based storage with user isolation
- Tier 2 (ephemeralState): Server-side cache with TTL support
- Update webpack externals to support subpath imports like @apache-superset/core/storage
- Add storage documentation and update architecture docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@michael-s-molina michael-s-molina force-pushed the extensions-storage-tiers-1-2 branch from 62203d4 to 93e3172 Compare April 10, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Related to the REST API change:backend Requires changing the backend change:frontend Requires changing the frontend dependencies:npm doc:developer Developer documentation doc Namespace | Anything related to documentation packages review:draft risk:db-migration PRs that require a DB migration size/XXL

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

4 participants