Skip to content

feat: add Electric Cloud managed service support#1572

Merged
saddlepaddle merged 1 commit into
mainfrom
electric-managed
Feb 18, 2026
Merged

feat: add Electric Cloud managed service support#1572
saddlepaddle merged 1 commit into
mainfrom
electric-managed

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Feb 18, 2026

Summary

  • Add header-based routing (X-Electric-Backend: cloud) in the API Electric proxy to support Electric Cloud alongside the existing self-hosted Fly.io backend
  • Add ELECTRIC_SOURCE_ID and ELECTRIC_SOURCE_SECRET env vars (optional, already set in production)
  • Desktop client sends the cloud header so new releases use Electric Cloud while older clients keep hitting Fly

Test plan

  • Deploy API — verify old clients without the header still route to Fly
  • Ship desktop update — verify new clients with the header route to Electric Cloud
  • Confirm shape sync works end-to-end through Electric Cloud (tasks, projects, members, etc.)
  • Once stable, tear down superset-electric Fly app and remove old env vars

Summary by CodeRabbit

  • New Features
    • Added Electric SQL cloud backend support, enabling seamless connection to the cloud-hosted Electric service as an alternative to self-hosted deployments.
    • Users can now configure the application to switch between cloud-backed and self-hosted Electric backends for real-time data synchronization based on infrastructure preferences.

…uting

Route Electric shape requests to Electric Cloud (api.electric-sql.cloud) when
clients send the `X-Electric-Backend: cloud` header, while preserving the
existing self-hosted Fly.io backend as the default for older clients.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

This pull request introduces cloud backend support for Electric SQL by adding conditional routing logic, new environment variables for cloud credentials, and a header to signal backend selection. The API route now routes requests to the cloud-hosted Electric service when configured, while maintaining backward compatibility with local backend deployments.

Changes

Cohort / File(s) Summary
Cloud Backend Configuration
apps/api/src/app/api/electric/[...path]/route.ts, apps/api/src/env.ts
Added conditional routing to detect cloud backend via x-electric-backend header; when cloud mode is enabled and credentials are present, requests are routed to https://api.electric-sql.cloud/v1/shape with source credentials as query parameters. Introduces ELECTRIC_SOURCE_ID and ELECTRIC_SOURCE_SECRET optional environment variables. Vary header updated to include X-Electric-Backend.
Client-Side Header Configuration
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts
Added static X-Electric-Backend: cloud header to all API requests via electricHeaders, enabling client-side signaling of cloud backend preference.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Poem

🐰 Through clouds so high, our queries fly,
With headers set and secrets nigh,
A fluffy route, so swift and free,
To the cloud backend we shall be! ☁️✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding support for Electric Cloud managed service, which is the primary objective of this pull request across all modified files.
Description check ✅ Passed The description includes a clear summary, test plan with specific verification steps, and additional context. However, it lacks structured sections matching the repository template (Related Issues, Type of Change, Testing, Screenshots, Additional Notes).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch electric-managed

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 18, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app
  • ✅ Streams Fly.io app

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/app/api/electric/`[...path]/route.ts:
- Around line 27-43: The cloud-auth query parameter is wrong and non-null
assertions are used unnecessarily: in the block building originUrl (symbols:
useCloud, originUrl) replace the query key "source_secret" with "secret" when
adding the cloud secret (env.ELECTRIC_SOURCE_SECRET) so requests authenticate
correctly, and to remove the biome-ignore and "!" assertions, first read
env.ELECTRIC_SOURCE_ID and env.ELECTRIC_SOURCE_SECRET into local consts and use
a boolean check (e.g., const hasCloudCreds = !!(sourceId && sourceSecret) &&
request.headers.get("x-electric-backend")==="cloud") to guard setting search
params; keep env.ELECTRIC_SECRET for the non-cloud branch.

Comment on lines +27 to +43
const useCloud =
request.headers.get("x-electric-backend") === "cloud" &&
env.ELECTRIC_SOURCE_ID &&
env.ELECTRIC_SOURCE_SECRET;

const originUrl = useCloud
? new URL("/v1/shape", "https://api.electric-sql.cloud")
: new URL(env.ELECTRIC_URL);

if (useCloud) {
// biome-ignore lint/style/noNonNullAssertion: guarded by useCloud check above
originUrl.searchParams.set("source_id", env.ELECTRIC_SOURCE_ID!);
// biome-ignore lint/style/noNonNullAssertion: guarded by useCloud check above
originUrl.searchParams.set("source_secret", env.ELECTRIC_SOURCE_SECRET!);
} else {
originUrl.searchParams.set("secret", env.ELECTRIC_SECRET);
}
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file in question
head -60 apps/api/src/app/api/electric/[...path]/route.ts | tail -40

Repository: superset-sh/superset

Length of output: 1330


🏁 Script executed:

# Search for Electric Cloud references in the codebase
rg -i "electric.*cloud" --type ts --type tsx --type js -C 3

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

# Search for Electric API parameter usage
rg "(source_secret|source_id|ELECTRIC_SOURCE)" --type ts --type tsx --type js -C 2

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

# Check for tests related to Electric Cloud
fd -e test.ts -e test.tsx -e spec.ts | xargs rg -l "electric" -i

Repository: superset-sh/superset

Length of output: 48


🏁 Script executed:

# Search for Electric Cloud references without file type restrictions
rg -i "source_secret|source_id" -C 2

Repository: superset-sh/superset

Length of output: 1627


🏁 Script executed:

# Search for ELECTRIC env vars and configuration
rg "ELECTRIC_SOURCE|ELECTRIC_URL|ELECTRIC_SECRET" -C 2

Repository: superset-sh/superset

Length of output: 6006


🏁 Script executed:

# Look for README, docs, or configuration files
fd -i "readme|doc|config|env" -e md -e txt -e example | head -20

Repository: superset-sh/superset

Length of output: 210


🏁 Script executed:

# Search for Electric Cloud references in comments or URLs
rg "electric-sql|electric.*cloud" -i

Repository: superset-sh/superset

Length of output: 8311


🏁 Script executed:

# Check if there's a package.json or any Electric-related dependencies
fd package.json -exec cat {} \; 2>/dev/null | head -100

Repository: superset-sh/superset

Length of output: 46


🌐 Web query:

Electric SQL Cloud API source_secret parameter documentation

💡 Result:

Electric Cloud’s HTTP API does not document a source_secret query parameter. For Cloud, you pass:

  • source_id — your Cloud “Source ID”
  • secret — your Cloud “Source secret” value, sent in the query string as secret=... (not source_secret=...) [1]

Example (from Electric Cloud docs): .../v1/shape?...&source_id=...&secret=... [1]

In the official HTTP API (OpenAPI) spec, the authentication parameter is also named secret (with the older api_secret marked deprecated) [2].

Sources: Electric Cloud Usage docs [1], Electric HTTP API (OpenAPI / Redoc) [2].


🌐 Web query:

Electric SQL Cloud v1/shape endpoint parameters authentication

💡 Result:

ElectricSQL Cloud GET https://api.electric-sql.cloud/v1/shape — auth + key query parameters

Authentication (Electric Cloud)

Electric Cloud authenticates requests using query parameters:

  • source_id — identifies your registered Postgres source
  • secret — the source secret/token granting access

Example: .../v1/shape?table=items&offset=-1&source_id=...&secret=... (don’t put the secret in a public client; add it in a backend/edge proxy). [1]

Core /v1/shape parameters (shape definition + syncing)

From the HTTP API spec, the commonly used query parameters are: [2][3]

  • table (required): root table, optionally schema-qualified (foo.issues), otherwise defaults to public. [2]
  • where (optional): PostgreSQL WHERE clause to filter rows. [2]
  • params[n] (optional): parameter values for positional placeholders in where (e.g., $1), passed as params[1]=.... [2]
  • columns (optional): comma-separated columns list to include. [2]

Sync control parameters:

  • offset (required):
    • -1 for initial sync from the beginning
    • a specific offset like 26800584_4 to continue
    • now to skip historical data and start “from current” [2]
  • handle (required when offset is not -1 or now): the electric-handle you got from the previous response. [2][3]
  • live (optional, boolean): set true to long-poll for realtime updates once up-to-date. [2][3]
  • cursor (optional): value from the previous response’s electric-cursor header (cache-busting for live polling). [2][3]
  • live_sse (optional, boolean): with live=true, use Server-Sent Events instead of long-polling. [2]

Change message shaping:

  • replica (optional: default | full): whether updates/deletes include only changed fields vs full rows. [2]
  • log (optional: full | changes_only): whether to stream an initial snapshot or only changes. [2]

Subset snapshot parameters (optional)

The spec also defines subset__* parameters (e.g., subset__where, subset__params, subset__limit, subset__offset, subset__order_by) for requesting subset snapshots. [2]

Sources: Electric Cloud Usage docs [1], Electric HTTP API (OpenAPI/Redoc) [2], Client development guide (handle/offset/live/cursor flow) [3].


🏁 Script executed:

# Search for any tests or examples using the Electric Cloud endpoint
rg "api\.electric-sql\.cloud" -C 3

Repository: superset-sh/superset

Length of output: 573


🏁 Script executed:

# Check the complete route.ts file to understand the full context
wc -l apps/api/src/app/api/electric/[...path]/route.ts

Repository: superset-sh/superset

Length of output: 115


🏁 Script executed:

# Look for any comments or documentation about Electric Cloud
rg -B 5 -A 5 "useCloud|electric-backend"

Repository: superset-sh/superset

Length of output: 3556


Change the Electric Cloud secret parameter name from source_secret to secret.

The code incorrectly uses source_secret as the query parameter name. According to the official Electric Cloud API documentation, the parameter must be named secret:

.../v1/shape?table=...&source_id=...&secret=...

The current line:

originUrl.searchParams.set("source_secret", env.ELECTRIC_SOURCE_SECRET!);

Should be:

originUrl.searchParams.set("secret", env.ELECTRIC_SOURCE_SECRET!);

With the current parameter name, all Cloud requests will fail authentication. This must be fixed before merging.

Additionally, the useCloud variable infers type string | false | undefined rather than boolean. This forces the two biome-ignore suppressions below. Extracting the env vars into local consts and using them directly in a single if branch eliminates the need for ! assertions and the suppression comments:

♻️ Optional refactor — avoid non-null assertions
-	const useCloud =
-		request.headers.get("x-electric-backend") === "cloud" &&
-		env.ELECTRIC_SOURCE_ID &&
-		env.ELECTRIC_SOURCE_SECRET;
-
-	const originUrl = useCloud
-		? new URL("/v1/shape", "https://api.electric-sql.cloud")
-		: new URL(env.ELECTRIC_URL);
-
-	if (useCloud) {
-		// biome-ignore lint/style/noNonNullAssertion: guarded by useCloud check above
-		originUrl.searchParams.set("source_id", env.ELECTRIC_SOURCE_ID!);
-		// biome-ignore lint/style/noNonNullAssertion: guarded by useCloud check above
-		originUrl.searchParams.set("source_secret", env.ELECTRIC_SOURCE_SECRET!);
-	} else {
-		originUrl.searchParams.set("secret", env.ELECTRIC_SECRET);
-	}
+	const sourceId = env.ELECTRIC_SOURCE_ID;
+	const sourceSecret = env.ELECTRIC_SOURCE_SECRET;
+	const isCloudRequest = request.headers.get("x-electric-backend") === "cloud";
+
+	let originUrl: URL;
+	if (isCloudRequest && sourceId !== undefined && sourceSecret !== undefined) {
+		originUrl = new URL("/v1/shape", "https://api.electric-sql.cloud");
+		originUrl.searchParams.set("source_id", sourceId);
+		originUrl.searchParams.set("secret", sourceSecret);
+	} else {
+		originUrl = new URL(env.ELECTRIC_URL);
+		originUrl.searchParams.set("secret", env.ELECTRIC_SECRET);
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/api/electric/`[...path]/route.ts around lines 27 - 43, The
cloud-auth query parameter is wrong and non-null assertions are used
unnecessarily: in the block building originUrl (symbols: useCloud, originUrl)
replace the query key "source_secret" with "secret" when adding the cloud secret
(env.ELECTRIC_SOURCE_SECRET) so requests authenticate correctly, and to remove
the biome-ignore and "!" assertions, first read env.ELECTRIC_SOURCE_ID and
env.ELECTRIC_SOURCE_SECRET into local consts and use a boolean check (e.g.,
const hasCloudCreds = !!(sourceId && sourceSecret) &&
request.headers.get("x-electric-backend")==="cloud") to guard setting search
params; keep env.ELECTRIC_SECRET for the non-cloud branch.

@saddlepaddle saddlepaddle merged commit 7964d37 into main Feb 18, 2026
13 of 15 checks passed
saddlepaddle added a commit that referenced this pull request Feb 19, 2026
…tric CORS

- Store agent command separately in PendingTerminalSetup instead of
  appending to initialCommands, so setup script and agent run in
  separate panes within the same tab
- Add X-Electric-Backend to CORS Access-Control-Allow-Headers (needed
  after Electric Cloud header was added in #1572)
- Use wildcard for Access-Control-Expose-Headers to expose all Electric
  response headers (electric-offset, electric-handle, electric-schema, etc.)
- Fix auth.apikeys where clause to use LIKE instead of ::jsonb cast
  which Docker Electric doesn't support
saddlepaddle added a commit that referenced this pull request Feb 19, 2026
* fix(desktop): run agent prompt in separate terminal pane and fix Electric CORS

- Store agent command separately in PendingTerminalSetup instead of
  appending to initialCommands, so setup script and agent run in
  separate panes within the same tab
- Add X-Electric-Backend to CORS Access-Control-Allow-Headers (needed
  after Electric Cloud header was added in #1572)
- Use wildcard for Access-Control-Expose-Headers to expose all Electric
  response headers (electric-offset, electric-handle, electric-schema, etc.)
- Fix auth.apikeys where clause to use LIKE instead of ::jsonb cast
  which Docker Electric doesn't support

* fix: explicitly list CORS expose headers and remove debug console.logs

- Replace wildcard Access-Control-Expose-Headers with explicit list
  (wildcard is ignored when Access-Control-Allow-Credentials is true)
- Remove stray console.log calls from trpc-storage.ts

* fix: remove empty line left by console.log removal
@Kitenite Kitenite deleted the electric-managed branch February 27, 2026 09:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant