Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
fde24cf
ui
jgoux Sep 6, 2024
02750cf
server implementation wip
jgoux Sep 6, 2024
a2fe5c6
tls
jgoux Sep 6, 2024
d6354f3
it works
jgoux Sep 6, 2024
69d5349
shareDatabase feature
jgoux Sep 9, 2024
4ca0b07
databaseUrl
jgoux Sep 9, 2024
6f5cdae
copyable field
jgoux Sep 9, 2024
2ae7124
background
jgoux Sep 9, 2024
f81c3a3
generalize certificate edge function
jgoux Sep 9, 2024
85f0274
wip
jgoux Sep 10, 2024
aa8d26c
fix deno dep import
jgoux Sep 10, 2024
e7f8740
small tweaks
jgoux Sep 10, 2024
7985dcb
bump
jgoux Sep 10, 2024
cff9289
apply ui tweaks
jgoux Sep 11, 2024
990aa8c
live presence and ui tweaks
jgoux Sep 11, 2024
a1abe7e
tweaks
jgoux Sep 11, 2024
1887a70
proxy protocol support
jgoux Sep 11, 2024
61b5252
add env example
jgoux Sep 11, 2024
74230e2
console -> debug
jgoux Sep 11, 2024
dfd9bd5
z-index trickery
jgoux Sep 11, 2024
898c34c
fix clientIp message interception
jgoux Sep 12, 2024
a13ff4e
fix type error
jgoux Sep 12, 2024
9e082dd
type check
jgoux Sep 12, 2024
01f77db
fix pg-gateway version
jgoux Sep 12, 2024
172106e
use the original socket to get the remoteAddress
jgoux Sep 12, 2024
8adbc16
bump deps
jgoux Sep 12, 2024
0b45b9f
tls
jgoux Sep 13, 2024
e1c9f1f
remove unused import
jgoux Sep 13, 2024
3e24ea7
fix attributes
jgoux Sep 13, 2024
b29c682
clean state between clients session
jgoux Sep 13, 2024
0779d7f
bump pg-gateway
jgoux Sep 17, 2024
edb1958
Merge pull request #100 from supabase-community/feat/db-sharing-pg-ga…
jgoux Sep 17, 2024
d7e59db
rename bucket
jgoux Sep 17, 2024
ebff0b9
basic telemetry
jgoux Sep 23, 2024
294f9c8
fix: ensure only one message/response occurs at a time
gregnr Sep 23, 2024
1f602e0
connectionId
jgoux Sep 24, 2024
08a8a43
prevent server crash
jgoux Sep 24, 2024
ea23c71
fix
jgoux Sep 24, 2024
ba37a9b
prisma
jgoux Sep 24, 2024
1dfb0ed
remove prisma
jgoux Sep 24, 2024
b438b86
use a Set
jgoux Sep 24, 2024
36f6616
auth gated live sharing
jgoux Sep 25, 2024
415a473
refactor
jgoux Sep 25, 2024
6cd8637
fix logic
jgoux Sep 25, 2024
7962452
lazy evaluation messages when debug is off
jgoux Sep 25, 2024
05efd70
bump pglite
jgoux Sep 25, 2024
49da099
fix: serialized json type in pglite
gregnr Sep 25, 2024
58fe271
weird
jgoux Sep 25, 2024
770f337
upgrade pgglite
jgoux Sep 30, 2024
87a7116
it works
jgoux Sep 30, 2024
d809d3e
skip if oid is correct
jgoux Sep 30, 2024
b906560
use last version of PGlite
jgoux Oct 1, 2024
0ad5bea
refactor middleware
jgoux Oct 1, 2024
b30268e
comment the middleware
jgoux Oct 1, 2024
68d37be
gracefully exit for --watch
jgoux Oct 1, 2024
62a4bbc
Merge pull request #105 from supabase-community/feat/db-sharing-pg-dump
jgoux Oct 1, 2024
c47a434
Merge pull request #103 from supabase-community/feat/db-sharing-conne…
jgoux Oct 1, 2024
cf49ddc
Merge branch 'main' into feat/db-sharing
jgoux Oct 4, 2024
fd0000e
add timeouts
jgoux Oct 4, 2024
31123ac
send final error message on timeout
jgoux Oct 4, 2024
8390bfb
fix patching logic
jgoux Oct 7, 2024
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
17 changes: 16 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
{
"deno.enablePaths": ["supabase/functions"],
"deno.lint": true,
"deno.unstable": true
"deno.unstable": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
11 changes: 11 additions & 0 deletions apps/browser-proxy/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
AWS_ACCESS_KEY_ID="<aws-access-key-id>"
AWS_ENDPOINT_URL_S3="<aws-endpoint-url-s3>"
AWS_S3_BUCKET=storage
AWS_SECRET_ACCESS_KEY="<aws-secret-access-key>"
AWS_REGION=us-east-1
LOGFLARE_SOURCE_URL="<logflare-source-url>"
# enable PROXY protocol support
#PROXIED=true
SUPABASE_URL="<supabase-url>"
SUPABASE_ANON_KEY="<supabase-anon-key>"
WILDCARD_DOMAIN=browser.staging.db.build
1 change: 1 addition & 0 deletions apps/browser-proxy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tls
13 changes: 13 additions & 0 deletions apps/browser-proxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM node:22-alpine

WORKDIR /app

COPY --link package.json ./
COPY --link src/ ./src/

RUN npm install

EXPOSE 443
EXPOSE 5432

CMD ["node", "--experimental-strip-types", "src/index.ts"]
33 changes: 33 additions & 0 deletions apps/browser-proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Browser Proxy

This app is a proxy that sits between the browser and a PostgreSQL client.

It is using a WebSocket server and a TCP server to make the communication between the PGlite instance in the browser and a standard PostgreSQL client possible.

## Development

Copy the `.env.example` file to `.env` and set the correct environment variables.

Install dependencies:

```sh
npm install
```

Start the proxy in development mode:

```sh
npm run dev
```

## Deployment

Create a new app on Fly.io, for example `database-build-browser-proxy`.

Fill the app's secrets with the correct environment variables based on the `.env.example` file.

Deploy the app:

```sh
fly deploy --app database-build-browser-proxy
```
23 changes: 23 additions & 0 deletions apps/browser-proxy/fly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
primary_region = 'iad'

[[services]]
internal_port = 5432
protocol = "tcp"
[[services.ports]]
handlers = ["proxy_proto"]
port = 5432

[[services]]
internal_port = 443
protocol = "tcp"
[[services.ports]]
port = 443

[[restart]]
policy = "always"
retries = 10

[[vm]]
memory = '512mb'
cpu_kind = 'shared'
cpus = 1
26 changes: 26 additions & 0 deletions apps/browser-proxy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@database.build/browser-proxy",
"type": "module",
"scripts": {
"start": "node --env-file=.env --experimental-strip-types src/index.ts",
"dev": "node --watch --env-file=.env --experimental-strip-types src/index.ts",
"type-check": "tsc"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.645.0",
"@supabase/supabase-js": "^2.45.4",
"debug": "^4.3.7",
"expiry-map": "^2.0.0",
"findhit-proxywrap": "^0.3.13",
"nanoid": "^5.0.7",
"p-memoize": "^7.1.1",
"pg-gateway": "^0.3.0-beta.3",
"ws": "^8.18.0"
},
"devDependencies": {
"@total-typescript/tsconfig": "^1.0.4",
"@types/debug": "^4.1.12",
"@types/node": "^22.5.4",
"typescript": "^5.5.4"
}
}
58 changes: 58 additions & 0 deletions apps/browser-proxy/src/connection-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { PostgresConnection } from 'pg-gateway'
import type { WebSocket } from 'ws'

type DatabaseId = string
type ConnectionId = string

class ConnectionManager {
private socketsByDatabase: Map<DatabaseId, ConnectionId> = new Map()
private sockets: Map<ConnectionId, PostgresConnection> = new Map()
private websockets: Map<DatabaseId, WebSocket> = new Map()

constructor() {}

public hasSocketForDatabase(databaseId: DatabaseId) {
return this.socketsByDatabase.has(databaseId)
}

public getSocket(connectionId: ConnectionId) {
return this.sockets.get(connectionId)
}

public getSocketForDatabase(databaseId: DatabaseId) {
const connectionId = this.socketsByDatabase.get(databaseId)
return connectionId ? this.sockets.get(connectionId) : undefined
}

public setSocket(databaseId: DatabaseId, connectionId: ConnectionId, socket: PostgresConnection) {
this.sockets.set(connectionId, socket)
this.socketsByDatabase.set(databaseId, connectionId)
}

public deleteSocketForDatabase(databaseId: DatabaseId) {
const connectionId = this.socketsByDatabase.get(databaseId)
this.socketsByDatabase.delete(databaseId)
if (connectionId) {
this.sockets.delete(connectionId)
}
}

public hasWebsocket(databaseId: DatabaseId) {
return this.websockets.has(databaseId)
}

public getWebsocket(databaseId: DatabaseId) {
return this.websockets.get(databaseId)
}

public setWebsocket(databaseId: DatabaseId, websocket: WebSocket) {
this.websockets.set(databaseId, websocket)
}

public deleteWebsocket(databaseId: DatabaseId) {
this.websockets.delete(databaseId)
this.deleteSocketForDatabase(databaseId)
}
}

export const connectionManager = new ConnectionManager()
55 changes: 55 additions & 0 deletions apps/browser-proxy/src/create-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export function createStartupMessage(
user: string,
database: string,
additionalParams: Record<string, string> = {}
): Uint8Array {
const encoder = new TextEncoder()

// Protocol version number (3.0)
const protocolVersion = 196608

// Combine required and additional parameters
const params = {
user,
database,
...additionalParams,
}

// Calculate total message length
let messageLength = 4 // Protocol version
for (const [key, value] of Object.entries(params)) {
messageLength += key.length + 1 + value.length + 1
}
messageLength += 1 // Null terminator

const uint8Array = new Uint8Array(4 + messageLength)
const view = new DataView(uint8Array.buffer)

let offset = 0
view.setInt32(offset, messageLength + 4, false) // Total message length (including itself)
offset += 4
view.setInt32(offset, protocolVersion, false) // Protocol version number
offset += 4

// Write key-value pairs
for (const [key, value] of Object.entries(params)) {
uint8Array.set(encoder.encode(key), offset)
offset += key.length
uint8Array.set([0], offset++) // Null terminator for key
uint8Array.set(encoder.encode(value), offset)
offset += value.length
uint8Array.set([0], offset++) // Null terminator for value
}

uint8Array.set([0], offset) // Final null terminator

return uint8Array
}

export function createTerminateMessage(): Uint8Array {
const uint8Array = new Uint8Array(5)
const view = new DataView(uint8Array.buffer)
view.setUint8(0, 'X'.charCodeAt(0))
view.setUint32(1, 4, false)
return uint8Array
}
5 changes: 5 additions & 0 deletions apps/browser-proxy/src/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import createDebug from 'debug'

createDebug.formatters.e = (fn) => fn()

export const debug = createDebug('browser-proxy')
16 changes: 16 additions & 0 deletions apps/browser-proxy/src/extract-ip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { isIPv4 } from 'node:net'

export function extractIP(address: string): string {
if (isIPv4(address)) {
return address
}

// Check if it's an IPv4-mapped IPv6 address
const ipv4 = address.match(/::ffff:(\d+\.\d+\.\d+\.\d+)/)
if (ipv4) {
return ipv4[1]!
}

// We assume it's an IPv6 address
return address
}
6 changes: 6 additions & 0 deletions apps/browser-proxy/src/findhit-proxywrap.types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module 'findhit-proxywrap' {
const module = {
proxy: (net: typeof import('node:net')) => typeof net,
}
export default module
}
37 changes: 37 additions & 0 deletions apps/browser-proxy/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { httpsServer } from './websocket-server.ts'
import { tcpServer } from './tcp-server.ts'

process.on('unhandledRejection', (reason, promise) => {
console.error({ location: 'unhandledRejection', reason, promise })
})

process.on('uncaughtException', (error) => {
console.error({ location: 'uncaughtException', error })
})

httpsServer.listen(443, () => {
console.log('websocket server listening on port 443')
})

tcpServer.listen(5432, () => {
console.log('tcp server listening on port 5432')
})

const shutdown = async () => {
await Promise.allSettled([
new Promise<void>((res) =>
httpsServer.close(() => {
res()
})
),
new Promise<void>((res) =>
tcpServer.close(() => {
res()
})
),
])
process.exit(0)
}

process.on('SIGTERM', shutdown)
process.on('SIGINT', shutdown)
2 changes: 2 additions & 0 deletions apps/browser-proxy/src/pg-dump-middleware/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const VECTOR_OID = 99999
export const FIRST_NORMAL_OID = 16384
Loading