electric-proxy-auth

This skill builds on electric-shapes. Read it first for ShapeStream configuration.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "electric-proxy-auth" with this command: npx skills add electric-sql/electric/electric-sql-electric-electric-proxy-auth

This skill builds on electric-shapes. Read it first for ShapeStream configuration.

Electric — Proxy and Auth

Setup

import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from '@electric-sql/client'

// Server route (Next.js App Router example) export async function GET(request: Request) { const url = new URL(request.url) const originUrl = new URL('/v1/shape', process.env.ELECTRIC_URL)

// Only forward Electric protocol params — never table/where from client url.searchParams.forEach((value, key) => { if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(key)) { originUrl.searchParams.set(key, value) } })

// Server decides shape definition originUrl.searchParams.set('table', 'todos') originUrl.searchParams.set('secret', process.env.ELECTRIC_SOURCE_SECRET!)

const response = await fetch(originUrl) const headers = new Headers(response.headers) headers.delete('content-encoding') headers.delete('content-length')

return new Response(response.body, { status: response.status, statusText: response.statusText, headers, }) }

Client usage:

import { ShapeStream } from '@electric-sql/client'

const stream = new ShapeStream({ url: '/api/todos', // Points to your proxy, not Electric directly })

Core Patterns

Tenant isolation with WHERE params

// In proxy route — inject user context server-side const user = await getAuthUser(request) originUrl.searchParams.set('table', 'todos') originUrl.searchParams.set('where', 'org_id = $1') originUrl.searchParams.set('params[1]', user.orgId)

Auth token refresh on 401

const stream = new ShapeStream({ url: '/api/todos', headers: { Authorization: async () => Bearer ${await getToken()}, }, onError: async (error) => { if (error instanceof FetchError && error.status === 401) { const newToken = await refreshToken() return { headers: { Authorization: Bearer ${newToken} } } } return {} }, })

CORS configuration for cross-origin proxies

// In proxy response headers headers.set( 'Access-Control-Expose-Headers', 'electric-offset, electric-handle, electric-schema, electric-cursor' )

Subset security (AND semantics)

Electric combines the main shape WHERE (set in proxy) with subset WHERE (from POST body) using AND. Subsets can only narrow results, never widen them:

-- Main shape: WHERE org_id = $1 (set by proxy) -- Subset: WHERE status = 'active' (from client POST) -- Effective: WHERE org_id = $1 AND status = 'active'

Even WHERE 1=1 in the subset cannot bypass the main shape's WHERE.

Common Mistakes

CRITICAL Forwarding all client params to Electric

Wrong:

url.searchParams.forEach((value, key) => { originUrl.searchParams.set(key, value) })

Correct:

import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from '@electric-sql/client'

url.searchParams.forEach((value, key) => { if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(key)) { originUrl.searchParams.set(key, value) } }) originUrl.searchParams.set('table', 'todos')

Forwarding all params lets the client control table , where , and columns , accessing any Postgres table. Only forward ELECTRIC_PROTOCOL_QUERY_PARAMS .

Source: examples/proxy-auth/app/shape-proxy/route.ts

CRITICAL Not deleting content-encoding and content-length headers

Wrong:

return new Response(response.body, { status: response.status, headers: response.headers, })

Correct:

const headers = new Headers(response.headers) headers.delete('content-encoding') headers.delete('content-length') return new Response(response.body, { status: response.status, headers })

fetch() decompresses the response body but keeps the original content-encoding and content-length headers, causing browser decoding failures.

Source: examples/proxy-auth/app/shape-proxy/route.ts:49-56

CRITICAL Exposing ELECTRIC_SECRET or SOURCE_SECRET to browser

Wrong:

// Client-side code const url = /v1/shape?table=todos&secret=${import.meta.env.VITE_ELECTRIC_SOURCE_SECRET}

Correct:

// Server proxy only originUrl.searchParams.set('secret', process.env.ELECTRIC_SOURCE_SECRET!)

Bundlers like Vite expose VITE_* env vars to client code. The secret must only be injected server-side in the proxy.

Source: AGENTS.md:17-20

CRITICAL SQL injection in WHERE clause via string interpolation

Wrong:

originUrl.searchParams.set('where', org_id = '${user.orgId}')

Correct:

originUrl.searchParams.set('where', 'org_id = $1') originUrl.searchParams.set('params[1]', user.orgId)

String interpolation in WHERE clauses enables SQL injection. Use positional params ($1 , $2 ).

Source: website/docs/guides/auth.md

HIGH Not exposing Electric response headers via CORS

Wrong:

// No CORS header configuration — browser strips custom headers return new Response(response.body, { headers })

Correct:

headers.set( 'Access-Control-Expose-Headers', 'electric-offset, electric-handle, electric-schema, electric-cursor' ) return new Response(response.body, { headers })

The client throws MissingHeadersError if Electric response headers are stripped by CORS. Expose electric-offset , electric-handle , electric-schema , and electric-cursor .

Source: packages/typescript-client/src/error.ts:109-118

CRITICAL Calling Electric directly from production client

Wrong:

new ShapeStream({ url: 'https://my-electric.example.com/v1/shape', params: { table: 'todos' }, })

Correct:

new ShapeStream({ url: '/api/todos', // Your proxy route })

Electric's HTTP API is public by default with no auth. Always proxy through your server so the server controls shape definitions and injects secrets.

Source: AGENTS.md:19-20

See also: electric-shapes/SKILL.md — Shape URLs must point to proxy routes, not directly to Electric. See also: electric-deployment/SKILL.md — Production requires ELECTRIC_SECRET and proxy; dev uses ELECTRIC_INSECURE=true. See also: electric-postgres-security/SKILL.md — Proxy injects secrets that Postgres security enforces.

Version

Targets @electric-sql/client v1.5.10.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

blog-planner

No summary provided by upstream source.

Repository SourceNeeds Review
General

electric-shapes

No summary provided by upstream source.

Repository SourceNeeds Review
General

electric-debugging

No summary provided by upstream source.

Repository SourceNeeds Review
General

electric-new-feature

No summary provided by upstream source.

Repository SourceNeeds Review