Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions apps/api/src/app/api/electric/[...path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,23 @@ export async function GET(request: Request): Promise<Response> {
return new Response("Not a member of this organization", { status: 403 });
}

const originUrl = new URL(env.ELECTRIC_URL);
originUrl.searchParams.set("secret", env.ELECTRIC_SECRET);
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);
}
Comment on lines +27 to +43
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.


url.searchParams.forEach((value, key) => {
if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(key)) {
Expand Down Expand Up @@ -63,7 +78,7 @@ export async function GET(request: Request): Promise<Response> {
const response = await fetch(originUrl.toString());

const headers = new Headers(response.headers);
headers.append("Vary", "Authorization");
headers.append("Vary", "Authorization, X-Electric-Backend");

if (headers.get("content-encoding")) {
headers.delete("content-encoding");
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const env = createEnv({
DATABASE_URL_UNPOOLED: z.string(),
ELECTRIC_URL: z.string().url(),
ELECTRIC_SECRET: z.string().min(16),
ELECTRIC_SOURCE_ID: z.string().optional(),
ELECTRIC_SOURCE_SECRET: z.string().optional(),
BLOB_READ_WRITE_TOKEN: z.string(),
GOOGLE_CLIENT_ID: z.string().min(1),
GOOGLE_CLIENT_SECRET: z.string().min(1),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const electricHeaders = {
const token = getAuthToken();
return token ? `Bearer ${token}` : "";
},
"X-Electric-Backend": "cloud",
};

const organizationsCollection = createCollection(
Expand Down
Loading