Skip to content
Draft
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
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# shellcheck shell=bash
export GH_HOST=github.com
source_env_if_exists .envrc.local
5 changes: 5 additions & 0 deletions apps/engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./lib": {
"bun": "./src/lib/index.ts",
"types": "./dist/lib/index.d.ts",
"import": "./dist/lib/index.js"
},
"./cli": {
"bun": "./src/cli/command.ts",
"types": "./dist/cli/command.d.ts",
Expand Down
6 changes: 6 additions & 0 deletions apps/engine/src/__generated__/openapi.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions apps/engine/src/__generated__/openapi.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions apps/service/src/__generated__/openapi.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions apps/service/src/__generated__/openapi.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/visualizer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@codemirror/lang-sql": "^6.7.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.26.0",
"@electric-sql/pglite": "^0.2.0",
"@electric-sql/pglite": "^0.4.5",
"@stripe/sync-source-stripe": "workspace:*",
"codemirror": "^6.0.1",
"next": "^15",
Expand Down
12 changes: 12 additions & 0 deletions examples/browser-sync/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stripe Sync Engine — Browser</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
31 changes: 31 additions & 0 deletions examples/browser-sync/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@stripe/sync-example-browser",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@electric-sql/pglite": "^0.4.5",
"@stripe/sync-destination-postgres": "workspace:*",
"@stripe/sync-engine": "workspace:*",
"@stripe/sync-protocol": "workspace:*",
"@stripe/sync-source-stripe": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^4",
"buffer": "^6.0.3",
"os-browserify": "^0.3.0",
"path-browserify": "^1.0.1",
"stream-browserify": "^3.0.0",
"typescript": "^5",
"vite": "^6",
"vite-plugin-node-polyfills": "^0.26.0"
}
}
106 changes: 106 additions & 0 deletions examples/browser-sync/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useState, useRef, useCallback } from 'react'
import { startSync } from './lib/sync'

export default function App() {
const [apiKey, setApiKey] = useState('')
const [status, setStatus] = useState<'idle' | 'running' | 'error'>('idle')
const [messages, setMessages] = useState<string[]>([])
const [query, setQuery] = useState('')
const [queryResult, setQueryResult] = useState<string>('')
const abortRef = useRef<AbortController | null>(null)

const addMessage = useCallback((msg: string) => {
setMessages((prev) => [...prev.slice(-200), msg])
}, [])

const handleStart = async () => {
if (!apiKey) return
setStatus('running')
setMessages([])
abortRef.current = new AbortController()

try {
await startSync({
apiKey,
websocket: true,
signal: abortRef.current.signal,
onMessage: (msg: unknown) => {
const m = msg as { type?: string; record?: { stream?: string } }
if (m.type === 'record') {
addMessage(`record: ${m.record?.stream}`)
} else {
addMessage(JSON.stringify(m).slice(0, 120))
}
},
})
} catch (err) {
if ((err as Error).name !== 'AbortError') {
setStatus('error')
addMessage(`Error: ${(err as Error).message}`)
}
}
}

const handleStop = () => {
abortRef.current?.abort()
setStatus('idle')
}

return (
<div style={{ fontFamily: 'monospace', padding: '2rem', maxWidth: '900px', margin: '0 auto' }}>
<h1>Stripe Sync Engine — Browser</h1>

<div style={{ marginBottom: '1rem' }}>
<input
type="password"
placeholder="sk_live_... or sk_test_..."
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
style={{ width: '400px', padding: '0.5rem', fontFamily: 'monospace' }}
/>
{status === 'idle' ? (
<button onClick={handleStart} style={{ marginLeft: '0.5rem', padding: '0.5rem 1rem' }}>
Start Sync
</button>
) : (
<button onClick={handleStop} style={{ marginLeft: '0.5rem', padding: '0.5rem 1rem' }}>
Stop
</button>
)}
<span style={{ marginLeft: '1rem' }}>{status}</span>
</div>

<div
style={{
background: '#111',
color: '#0f0',
padding: '1rem',
height: '300px',
overflowY: 'auto',
fontSize: '12px',
marginBottom: '1rem',
}}
>
{messages.map((m, i) => (
<div key={i}>{m}</div>
))}
</div>

<div>
<textarea
placeholder="SELECT * FROM stripe.customers LIMIT 10"
value={query}
onChange={(e) => setQuery(e.target.value)}
style={{ width: '100%', height: '60px', fontFamily: 'monospace', padding: '0.5rem' }}
/>
<button
onClick={() => setQueryResult('TODO: wire PGlite query')}
style={{ padding: '0.5rem 1rem' }}
>
Run Query
</button>
{queryResult && <pre style={{ marginTop: '0.5rem' }}>{queryResult}</pre>}
</div>
</div>
)
}
49 changes: 49 additions & 0 deletions examples/browser-sync/src/lib/sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createEngine, createConnectorResolver } from '@stripe/sync-engine/lib'
import sourceStripe from '@stripe/sync-source-stripe'
import destinationPostgres from '@stripe/sync-destination-postgres/pglite'

export interface SyncOptions {
apiKey: string
websocket?: boolean
onMessage?: (msg: unknown) => void
signal?: AbortSignal
}

export async function startSync({ apiKey, websocket = true, onMessage, signal }: SyncOptions) {
const resolver = await createConnectorResolver({
sources: { stripe: sourceStripe },
destinations: { postgres: destinationPostgres },
})

const engine = createEngine(resolver)

const pipeline = {
source: {
type: 'stripe' as const,
stripe: {
api_key: apiKey,
websocket,
},
},
destination: {
type: 'postgres' as const,
postgres: {
url: 'memory://',
schema: 'stripe',
batch_size: 50,
},
},
}

// Setup (creates schema + tables)
for await (const msg of engine.pipeline_setup(pipeline)) {
onMessage?.(msg)
if (signal?.aborted) return
}

// Sync (backfill + live)
for await (const msg of engine.pipeline_sync(pipeline)) {
onMessage?.(msg)
if (signal?.aborted) return
}
}
4 changes: 4 additions & 0 deletions examples/browser-sync/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createRoot } from 'react-dom/client'
import App from './App'

createRoot(document.getElementById('root')!).render(<App />)
10 changes: 10 additions & 0 deletions examples/browser-sync/src/shims/child_process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function spawn() {
throw new Error('child_process.spawn is not available in browser')
}
export function exec() {
throw new Error('child_process.exec is not available in browser')
}
export function execSync() {
throw new Error('child_process.execSync is not available in browser')
}
export default { spawn, exec, execSync }
35 changes: 35 additions & 0 deletions examples/browser-sync/src/shims/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export function createHash(algorithm: string) {
let data = ''
return {
update(input: string) { data += input; return this },
async digest(encoding: string) {
const encoder = new TextEncoder()
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(data))
const hashArray = Array.from(new Uint8Array(hashBuffer))
if (encoding === 'hex') return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
return hashArray
},
}
}

export function createHmac(algorithm: string, key: string) {
let data = ''
return {
update(input: string) { data += input; return this },
digest(encoding: string) {
// Synchronous HMAC not available in browser — return placeholder
// This is only used for webhook verification which isn't needed in browser
console.warn('createHmac: browser shim — webhook verification disabled')
return ''
},
}
}

export function timingSafeEqual(a: Uint8Array, b: Uint8Array) {
if (a.length !== b.length) return false
let result = 0
for (let i = 0; i < a.length; i++) result |= a[i]! ^ b[i]!
return result === 0
}

export default { createHash, createHmac, timingSafeEqual }
4 changes: 4 additions & 0 deletions examples/browser-sync/src/shims/logger-progress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function formatProgress() { return '' }
export function formatProgressHeader() { return '' }
export function ProgressView() { return null }
export function ProgressHeader() { return null }
Loading
Loading