├── supabase
├── seed.sql
├── .gitignore
├── functions
│ ├── deno.json
│ ├── .env.example
│ └── certificate
│ │ ├── env.ts
│ │ └── README.md
└── migrations
│ ├── 20240810203851_remove_waitlist_delete_policy.sql
│ ├── 20240909160000_pg_cron.sql
│ ├── 20240807150202_rename_publish_waitlist.sql
│ ├── 20241115093122_enable_rls_deployment_providers.sql
│ ├── 20240909155000_expose_functions_certificate_secrets.sql
│ └── 20240806202109_publish_waitlist.sql
├── apps
├── browser-proxy
│ ├── .gitignore
│ ├── src
│ │ ├── pg-dump-middleware
│ │ │ ├── constants.ts
│ │ │ └── utils.ts
│ │ ├── debug.ts
│ │ ├── findhit-proxywrap.types.d.ts
│ │ ├── extract-ip.ts
│ │ ├── servername.ts
│ │ ├── protocol.ts
│ │ ├── index.ts
│ │ ├── tls.ts
│ │ ├── telemetry.ts
│ │ ├── create-message.ts
│ │ └── connection-manager.ts
│ ├── Dockerfile
│ ├── tsconfig.json
│ ├── .env.example
│ ├── package.json
│ └── README.md
├── web
│ ├── .gitignore
│ ├── components
│ │ ├── ai-icon-animation
│ │ │ ├── index.tsx
│ │ │ └── ai-icon-animation.tsx
│ │ ├── framer-features.ts
│ │ ├── ui
│ │ │ ├── skeleton.tsx
│ │ │ ├── textarea.tsx
│ │ │ ├── label.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── progress.tsx
│ │ │ ├── input.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── tooltip.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── popover.tsx
│ │ │ ├── button.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── accordion.tsx
│ │ │ └── breadcrumb.tsx
│ │ ├── theme-provider.tsx
│ │ ├── schema
│ │ │ ├── graph.tsx
│ │ │ └── legend.tsx
│ │ ├── layout
│ │ │ └── header
│ │ │ │ ├── menu-button.tsx
│ │ │ │ ├── bring-your-own-llm-button.tsx
│ │ │ │ ├── create-database-button.tsx
│ │ │ │ ├── deploy-button
│ │ │ │ ├── connect-supabase-tab.tsx
│ │ │ │ ├── redeploy-supabase-tab.tsx
│ │ │ │ └── deploy-supabase-tab.tsx
│ │ │ │ ├── live-share-button.tsx
│ │ │ │ ├── header.tsx
│ │ │ │ ├── toggle-theme-button.tsx
│ │ │ │ ├── extra-database-actions-button.tsx
│ │ │ │ └── user.tsx
│ │ ├── sign-in-button.tsx
│ │ ├── deploy
│ │ │ ├── deploy-failure-dialog.tsx
│ │ │ ├── deploy-success-dialog.tsx
│ │ │ ├── integration-dialog.tsx
│ │ │ ├── schema-overlap-warning.tsx
│ │ │ ├── deploy-info-dialog.tsx
│ │ │ ├── redeploy-dialog.tsx
│ │ │ └── deploy-info.tsx
│ │ ├── tools
│ │ │ ├── csv-import.tsx
│ │ │ ├── sql-import.tsx
│ │ │ ├── generated-embedding.tsx
│ │ │ ├── conversation-rename.tsx
│ │ │ ├── generated-chart.tsx
│ │ │ ├── index.tsx
│ │ │ ├── csv-export.tsx
│ │ │ └── executed-sql.tsx
│ │ ├── live-share-icon.tsx
│ │ ├── model-provider
│ │ │ └── use-model-provider.ts
│ │ ├── providers.tsx
│ │ ├── byo-llm-button.tsx
│ │ ├── theme-dropdown.tsx
│ │ ├── sidebar
│ │ │ ├── database-menu-item.tsx
│ │ │ ├── sign-in-dialog.tsx
│ │ │ └── set-external-model-provider-button.tsx
│ │ ├── copyable-field.tsx
│ │ ├── supabase-icon.tsx
│ │ ├── markdown-accordion.tsx
│ │ ├── code-accordion.tsx
│ │ ├── layout.tsx
│ │ └── lines.tsx
│ ├── app
│ │ ├── favicon.ico
│ │ ├── apple-icon.png
│ │ ├── opengraph-image.png
│ │ ├── (main)
│ │ │ ├── layout.tsx
│ │ │ └── db
│ │ │ │ └── [id]
│ │ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── public
│ │ ├── images
│ │ │ └── empty.png
│ │ └── fonts
│ │ │ ├── custom
│ │ │ ├── CustomFont-Bold.woff
│ │ │ ├── CustomFont-Book.woff
│ │ │ ├── CustomFont-Black.woff
│ │ │ ├── CustomFont-Black.woff2
│ │ │ ├── CustomFont-Bold.woff2
│ │ │ ├── CustomFont-Book.woff2
│ │ │ ├── CustomFont-Medium.woff
│ │ │ ├── CustomFont-Medium.woff2
│ │ │ ├── CustomFont-BlackItalic.woff
│ │ │ ├── CustomFont-BlackItalic.woff2
│ │ │ ├── CustomFont-BoldItalic.woff
│ │ │ ├── CustomFont-BoldItalic.woff2
│ │ │ ├── CustomFont-BookItalic.woff
│ │ │ └── CustomFont-BookItalic.woff2
│ │ │ └── source-code-pro
│ │ │ ├── SourceCodePro-Regular.eot
│ │ │ ├── SourceCodePro-Regular.ttf
│ │ │ ├── SourceCodePro-Regular.woff
│ │ │ └── SourceCodePro-Regular.woff2
│ ├── types
│ │ └── highlightjs-curl.d.ts
│ ├── lib
│ │ ├── utils.ts
│ │ ├── embed
│ │ │ ├── worker.ts
│ │ │ └── index.ts
│ │ ├── llm-provider.ts
│ │ ├── websocket-protocol.ts
│ │ ├── use-breakpoint.ts
│ │ ├── files.ts
│ │ ├── db
│ │ │ └── worker.ts
│ │ ├── system-prompt.ts
│ │ └── schema.ts
│ ├── postcss.config.mjs
│ ├── global.d.ts
│ ├── docker-compose.yml
│ ├── utils
│ │ ├── supabase
│ │ │ ├── client.ts
│ │ │ ├── admin.ts
│ │ │ ├── server.ts
│ │ │ └── middleware.ts
│ │ └── telemetry.ts
│ ├── middleware.ts
│ ├── components.json
│ ├── vite.config.mts
│ ├── hooks
│ │ └── use-mobile.tsx
│ ├── data
│ │ ├── databases
│ │ │ ├── databases-query.ts
│ │ │ ├── database-query.ts
│ │ │ ├── database-create-mutation.ts
│ │ │ ├── database-delete-mutation.ts
│ │ │ └── database-update-mutation.ts
│ │ ├── messages
│ │ │ ├── messages-query.ts
│ │ │ └── message-create-mutation.ts
│ │ ├── merged-databases
│ │ │ ├── merged-databases.ts
│ │ │ └── merged-database.ts
│ │ ├── deploy-waitlist
│ │ │ ├── deploy-waitlist-create-mutation.ts
│ │ │ └── deploy-waitlist-query.ts
│ │ ├── deployed-databases
│ │ │ └── deployed-databases-query.ts
│ │ ├── integrations
│ │ │ └── integration-query.ts
│ │ └── tables
│ │ │ └── tables-query.ts
│ ├── tsconfig.json
│ ├── .env.example
│ ├── assets
│ │ └── github-icon.tsx
│ ├── next.config.mjs
│ ├── sw.ts
│ ├── README.md
│ ├── polyfills
│ │ └── readable-stream.ts
│ └── config
│ │ └── default-colors.js
└── deploy-worker
│ ├── tsconfig.json
│ ├── README.md
│ ├── .env.example
│ ├── .gitignore
│ ├── Dockerfile
│ ├── package.json
│ └── src
│ └── index.ts
├── .npmrc
├── packages
└── deploy
│ ├── src
│ ├── index.ts
│ ├── error.ts
│ └── supabase
│ │ ├── schemas.ts
│ │ ├── index.ts
│ │ ├── generate-password.ts
│ │ ├── management-api
│ │ └── client.ts
│ │ ├── revoke-integration.ts
│ │ ├── get-database-url.ts
│ │ ├── types.ts
│ │ └── get-access-token.ts
│ ├── tsconfig.json
│ ├── tsup.config.ts
│ └── package.json
├── .eslintrc.json
├── .dockerignore
├── vercel.json
├── .vscode
├── extensions.json
└── settings.json
├── .env.example
├── package.json
├── fly.deploy-worker.toml
├── .prettierrc
├── fly.browser-proxy.toml
├── .gitignore
├── .github
└── workflows
│ └── run-migrations.yml
└── turbo.json
/supabase/seed.sql:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/browser-proxy/.gitignore:
--------------------------------------------------------------------------------
1 | tls
--------------------------------------------------------------------------------
/apps/web/.gitignore:
--------------------------------------------------------------------------------
1 | public/sw.mjs
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | @jsr:registry=https://npm.jsr.io
2 |
--------------------------------------------------------------------------------
/packages/deploy/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './error.js'
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 | .env
5 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/.next
3 | **/.turbo
4 | **/.env*
5 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "buildCommand": "DO_NOT_TRACK=1 turbo build"
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["denoland.vscode-deno"]
3 | }
4 |
--------------------------------------------------------------------------------
/apps/web/components/ai-icon-animation/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './ai-icon-animation'
2 |
--------------------------------------------------------------------------------
/supabase/functions/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@database-build/supabase-functions"
3 | }
4 |
--------------------------------------------------------------------------------
/apps/web/components/framer-features.ts:
--------------------------------------------------------------------------------
1 | import { domMax } from 'framer-motion'
2 | export default domMax
3 |
--------------------------------------------------------------------------------
/apps/web/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/app/favicon.ico
--------------------------------------------------------------------------------
/apps/web/app/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/app/apple-icon.png
--------------------------------------------------------------------------------
/apps/browser-proxy/src/pg-dump-middleware/constants.ts:
--------------------------------------------------------------------------------
1 | export const VECTOR_OID = 99999
2 | export const FIRST_NORMAL_OID = 16384
3 |
--------------------------------------------------------------------------------
/apps/web/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/app/opengraph-image.png
--------------------------------------------------------------------------------
/apps/web/public/images/empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/images/empty.png
--------------------------------------------------------------------------------
/apps/web/types/highlightjs-curl.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'highlightjs-curl' {
2 | const languageFunc: any
3 | export = languageFunc
4 | }
5 |
--------------------------------------------------------------------------------
/apps/web/public/fonts/custom/CustomFont-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/custom/CustomFont-Bold.woff
--------------------------------------------------------------------------------
/apps/web/public/fonts/custom/CustomFont-Book.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/custom/CustomFont-Book.woff
--------------------------------------------------------------------------------
/supabase/migrations/20240810203851_remove_waitlist_delete_policy.sql:
--------------------------------------------------------------------------------
1 | drop policy if exists "Users can only delete their own waitlist record." on public.deploy_waitlist;
--------------------------------------------------------------------------------
/apps/web/public/fonts/custom/CustomFont-Black.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/custom/CustomFont-Black.woff
--------------------------------------------------------------------------------
/apps/web/public/fonts/custom/CustomFont-Black.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/custom/CustomFont-Black.woff2
--------------------------------------------------------------------------------
/apps/web/public/fonts/custom/CustomFont-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/custom/CustomFont-Bold.woff2
--------------------------------------------------------------------------------
/apps/web/public/fonts/custom/CustomFont-Book.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/custom/CustomFont-Book.woff2
--------------------------------------------------------------------------------
/apps/web/public/fonts/custom/CustomFont-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/custom/CustomFont-Medium.woff
--------------------------------------------------------------------------------
/apps/web/public/fonts/custom/CustomFont-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/custom/CustomFont-Medium.woff2
--------------------------------------------------------------------------------
/apps/browser-proxy/src/debug.ts:
--------------------------------------------------------------------------------
1 | import createDebug from 'debug'
2 |
3 | createDebug.formatters.e = (fn) => fn()
4 |
5 | export const debug = createDebug('browser-proxy')
6 |
--------------------------------------------------------------------------------
/apps/web/public/fonts/custom/CustomFont-BlackItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/custom/CustomFont-BlackItalic.woff
--------------------------------------------------------------------------------
/apps/web/public/fonts/custom/CustomFont-BlackItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/custom/CustomFont-BlackItalic.woff2
--------------------------------------------------------------------------------
/apps/web/public/fonts/custom/CustomFont-BoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/custom/CustomFont-BoldItalic.woff
--------------------------------------------------------------------------------
/apps/web/public/fonts/custom/CustomFont-BoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/custom/CustomFont-BoldItalic.woff2
--------------------------------------------------------------------------------
/apps/web/public/fonts/custom/CustomFont-BookItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/custom/CustomFont-BookItalic.woff
--------------------------------------------------------------------------------
/apps/web/public/fonts/custom/CustomFont-BookItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/custom/CustomFont-BookItalic.woff2
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | SUPABASE_AUTH_GITHUB_CLIENT_ID=github-client-id
2 | SUPABASE_AUTH_GITHUB_SECRET=github-secret
3 | SUPABASE_AUTH_GITHUB_REDIRECT_URI=http://localhost:54321/auth/v1/callback
4 |
--------------------------------------------------------------------------------
/apps/web/public/fonts/source-code-pro/SourceCodePro-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/source-code-pro/SourceCodePro-Regular.eot
--------------------------------------------------------------------------------
/apps/web/public/fonts/source-code-pro/SourceCodePro-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/source-code-pro/SourceCodePro-Regular.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/source-code-pro/SourceCodePro-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/source-code-pro/SourceCodePro-Regular.woff
--------------------------------------------------------------------------------
/apps/web/public/fonts/source-code-pro/SourceCodePro-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supabase-community/database-build/HEAD/apps/web/public/fonts/source-code-pro/SourceCodePro-Regular.woff2
--------------------------------------------------------------------------------
/supabase/migrations/20240909160000_pg_cron.sql:
--------------------------------------------------------------------------------
1 | create extension pg_cron with schema extensions;
2 | grant usage on schema cron to postgres;
3 | grant all privileges on all tables in schema cron to postgres;
--------------------------------------------------------------------------------
/apps/browser-proxy/src/findhit-proxywrap.types.d.ts:
--------------------------------------------------------------------------------
1 | module 'findhit-proxywrap' {
2 | const module = {
3 | proxy: (net: typeof import('node:net')) => typeof net,
4 | }
5 | export default module
6 | }
7 |
--------------------------------------------------------------------------------
/apps/web/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/packages/deploy/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@total-typescript/tsconfig/tsc/no-dom/app",
3 | "include": ["src/**/*.ts"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "outDir": "dist"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/apps/web/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | autoprefixer: {},
6 | },
7 | }
8 |
9 | export default config
10 |
--------------------------------------------------------------------------------
/supabase/migrations/20240807150202_rename_publish_waitlist.sql:
--------------------------------------------------------------------------------
1 | alter table public.publish_waitlist
2 | rename to deploy_waitlist;
3 |
4 | alter table public.deploy_waitlist rename constraint publish_waitlist_user_id_fkey to deploy_waitlist_user_id_fkey;
--------------------------------------------------------------------------------
/apps/web/app/(main)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Layout from '~/components/layout'
2 |
3 | export default function MainLayout({
4 | children,
5 | }: Readonly<{
6 | children: React.ReactNode
7 | }>) {
8 | return {children}
9 | }
10 |
--------------------------------------------------------------------------------
/apps/web/global.d.ts:
--------------------------------------------------------------------------------
1 | declare interface ReadableStream extends AsyncIterable {
2 | values(options?: ReadableStreamIteratorOptions): AsyncIterator
3 | [Symbol.asyncIterator](options?: ReadableStreamIteratorOptions): AsyncIterator
4 | }
5 |
--------------------------------------------------------------------------------
/apps/browser-proxy/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:22-alpine
2 |
3 | WORKDIR /app
4 |
5 | COPY --link package.json ./
6 | COPY --link src/ ./src/
7 |
8 | RUN npm install
9 |
10 | EXPOSE 443
11 | EXPOSE 5432
12 |
13 | CMD ["node", "--experimental-strip-types", "src/index.ts"]
--------------------------------------------------------------------------------
/apps/browser-proxy/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@total-typescript/tsconfig/tsc/no-dom/app",
3 | "include": ["src/**/*.ts"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "allowImportingTsExtensions": true,
7 | "outDir": "dist"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/apps/deploy-worker/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@total-typescript/tsconfig/tsc/no-dom/app",
3 | "include": ["src/**/*.ts"],
4 | "compilerOptions": {
5 | "allowImportingTsExtensions": true,
6 | "noEmit": true,
7 | "outDir": "dist"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/apps/deploy-worker/README.md:
--------------------------------------------------------------------------------
1 | ## Development
2 |
3 | Copy the `.env.example` file to `.env` and set the correct environment variables.
4 |
5 | Run the dev server from the monorepo root. See [Development](../../README.md#development).
6 |
7 | The deploy worker will be listening on port `4000` (HTTP).
8 |
--------------------------------------------------------------------------------
/apps/web/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | redis:
3 | image: redis
4 | local-vercel-kv:
5 | image: hiett/serverless-redis-http:latest
6 | ports:
7 | - 8080:80
8 | environment:
9 | SRH_MODE: env
10 | SRH_TOKEN: local_token
11 | SRH_CONNECTION_STRING: redis://redis:6379
12 |
--------------------------------------------------------------------------------
/apps/web/utils/supabase/client.ts:
--------------------------------------------------------------------------------
1 | import { createBrowserClient } from '@supabase/ssr'
2 | import { Database } from './db-types'
3 |
4 | export function createClient() {
5 | return createBrowserClient(
6 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
7 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/packages/deploy/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup'
2 |
3 | export default defineConfig([
4 | {
5 | entry: ['src/index.ts', 'src/supabase/index.ts'],
6 | format: ['cjs', 'esm'],
7 | outDir: 'dist',
8 | sourcemap: true,
9 | dts: true,
10 | minify: true,
11 | splitting: true,
12 | },
13 | ])
14 |
--------------------------------------------------------------------------------
/apps/web/utils/supabase/admin.ts:
--------------------------------------------------------------------------------
1 | import { createClient as createSupabaseClient } from '@supabase/supabase-js'
2 | import { Database } from './db-types'
3 |
4 | export function createClient() {
5 | return createSupabaseClient(
6 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
7 | process.env.SUPABASE_SERVICE_ROLE_KEY!
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/apps/web/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "~/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@database.build/root",
3 | "private": true,
4 | "packageManager": "npm@10.8.3",
5 | "scripts": {
6 | "dev": "turbo watch dev",
7 | "build": "turbo run build"
8 | },
9 | "workspaces": ["apps/*", "packages/*"],
10 | "devDependencies": {
11 | "supabase": "^2.0.0",
12 | "turbo": "^2.3.2"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/fly.deploy-worker.toml:
--------------------------------------------------------------------------------
1 | primary_region = 'iad'
2 |
3 | [build]
4 | dockerfile = "./apps/deploy-worker/Dockerfile"
5 |
6 | [http_service]
7 | internal_port = 4000
8 | force_https = true
9 | auto_stop_machines = "suspend"
10 | auto_start_machines = true
11 | min_machines_running = 0
12 |
13 | [[vm]]
14 | memory = '512mb'
15 | cpu_kind = 'shared'
16 | cpus = 1
17 |
--------------------------------------------------------------------------------
/supabase/functions/.env.example:
--------------------------------------------------------------------------------
1 | ACME_DIRECTORY_URL=https://acme-staging-v02.api.letsencrypt.org/directory
2 | ACME_EMAIL=""
3 | AWS_ACCESS_KEY_ID=""
4 | AWS_ENDPOINT_URL_S3=http://172.17.0.1:54321/storage/v1/s3
5 | AWS_S3_BUCKET=storage
6 | AWS_SECRET_ACCESS_KEY=""
7 | AWS_REGION=local
8 | CLOUDFLARE_API_TOKEN=""
--------------------------------------------------------------------------------
/supabase/migrations/20241115093122_enable_rls_deployment_providers.sql:
--------------------------------------------------------------------------------
1 | -- Enable RLS on deployment_providers to dismiss security warnings
2 | alter table deployment_providers enable row level security;
3 |
4 | -- RLS allow all policy for deployment_providers
5 | create policy "Allow all operations on deployment_providers"
6 | on deployment_providers
7 | for all
8 | using (true);
9 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": false,
5 | "singleQuote": true,
6 | "printWidth": 100,
7 | "endOfLine": "lf",
8 | "sqlKeywordCase": "lower",
9 | "pluginSearchDirs": false,
10 | "overrides": [
11 | {
12 | "files": "**/*.json",
13 | "options": {
14 | "parser": "json"
15 | }
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/apps/web/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { ThemeProvider as NextThemesProvider } from 'next-themes'
5 | import { type ThemeProviderProps } from 'next-themes/dist/types'
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children}
9 | }
10 |
--------------------------------------------------------------------------------
/apps/web/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest } from 'next/server'
2 | import { updateSession } from '~/utils/supabase/middleware'
3 |
4 | export async function middleware(request: NextRequest) {
5 | return await updateSession(request)
6 | }
7 |
8 | export const config = {
9 | matcher: [
10 | /*
11 | * Middleware only runs on API routes.
12 | */
13 | '/api/:path*',
14 | ],
15 | }
16 |
--------------------------------------------------------------------------------
/apps/deploy-worker/.env.example:
--------------------------------------------------------------------------------
1 | SUPABASE_ANON_KEY=""
2 | SUPABASE_OAUTH_CLIENT_ID=""
3 | SUPABASE_OAUTH_SECRET=""
4 | SUPABASE_SERVICE_ROLE_KEY=""
5 | SUPABASE_URL=""
6 | SUPABASE_PLATFORM_URL="https://supabase.com"
7 | SUPABASE_PLATFORM_API_URL="https://api.supabase.com"
8 | SUPABASE_PLATFORM_DEPLOY_REGION="us-east-1"
9 |
--------------------------------------------------------------------------------
/apps/web/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "~/components",
15 | "utils": "~/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/apps/browser-proxy/src/extract-ip.ts:
--------------------------------------------------------------------------------
1 | import { isIPv4 } from 'node:net'
2 |
3 | export function extractIP(address: string): string {
4 | if (isIPv4(address)) {
5 | return address
6 | }
7 |
8 | // Check if it's an IPv4-mapped IPv6 address
9 | const ipv4 = address.match(/::ffff:(\d+\.\d+\.\d+\.\d+)/)
10 | if (ipv4) {
11 | return ipv4[1]!
12 | }
13 |
14 | // We assume it's an IPv6 address
15 | return address
16 | }
17 |
--------------------------------------------------------------------------------
/apps/browser-proxy/.env.example:
--------------------------------------------------------------------------------
1 | AWS_ACCESS_KEY_ID=""
2 | AWS_ENDPOINT_URL_S3=""
3 | AWS_S3_BUCKET=storage
4 | AWS_SECRET_ACCESS_KEY=""
5 | AWS_REGION=us-east-1
6 | LOGFLARE_SOURCE_URL=""
7 | # enable PROXY protocol support
8 | #PROXIED=true
9 | SUPABASE_URL=""
10 | SUPABASE_ANON_KEY=""
11 | WILDCARD_DOMAIN=browser.staging.db.build
12 |
--------------------------------------------------------------------------------
/apps/deploy-worker/.gitignore:
--------------------------------------------------------------------------------
1 | # dev
2 | .yarn/
3 | !.yarn/releases
4 | .vscode/*
5 | !.vscode/launch.json
6 | !.vscode/*.code-snippets
7 | .idea/workspace.xml
8 | .idea/usage.statistics.xml
9 | .idea/shelf
10 |
11 | # deps
12 | node_modules/
13 |
14 | # env
15 | .env
16 | .env.production
17 |
18 | # logs
19 | logs/
20 | *.log
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | pnpm-debug.log*
25 | lerna-debug.log*
26 |
27 | # misc
28 | .DS_Store
29 |
--------------------------------------------------------------------------------
/packages/deploy/src/error.ts:
--------------------------------------------------------------------------------
1 | export class DeployError extends Error {
2 | constructor(message: string, options?: ErrorOptions) {
3 | super(message, options)
4 | }
5 | }
6 |
7 | export class IntegrationRevokedError extends Error {
8 | constructor(options?: ErrorOptions) {
9 | super(
10 | 'Your Supabase integration has been revoked. Please retry to restore your integration.',
11 | options
12 | )
13 | this.name = 'IntegrationRevokedError'
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/deploy/src/supabase/schemas.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Supabase built-in schemas that will be excluded from the
3 | * deployment if they exist in the source database.
4 | */
5 | export const SUPABASE_SCHEMAS = [
6 | 'auth',
7 | 'cron',
8 | 'extensions',
9 | 'graphql',
10 | 'graphql_public',
11 | 'net',
12 | 'pgbouncer',
13 | 'pgsodium',
14 | 'pgsodium_masks',
15 | 'realtime',
16 | 'storage',
17 | 'supabase_functions',
18 | 'supabase_migrations',
19 | 'vault',
20 | ]
21 |
--------------------------------------------------------------------------------
/packages/deploy/src/supabase/index.ts:
--------------------------------------------------------------------------------
1 | export * from './create-deployed-database.js'
2 | export * from './generate-password.js'
3 | export * from './get-access-token.js'
4 | export * from './get-database-url.js'
5 | export * from './revoke-integration.js'
6 | export * from './wait-for-health.js'
7 | export * from './database-types.js'
8 | export * from './schemas.js'
9 | export * from './types.js'
10 |
11 | export * from './management-api/client.js'
12 | export * from './management-api/types.js'
13 |
--------------------------------------------------------------------------------
/fly.browser-proxy.toml:
--------------------------------------------------------------------------------
1 | primary_region = 'iad'
2 |
3 | [build]
4 | dockerfile = "./apps/browser-proxy/Dockerfile"
5 |
6 | [[services]]
7 | internal_port = 5432
8 | protocol = "tcp"
9 | [[services.ports]]
10 | handlers = ["proxy_proto"]
11 | port = 5432
12 |
13 | [[services]]
14 | internal_port = 443
15 | protocol = "tcp"
16 | [[services.ports]]
17 | port = 443
18 |
19 | [[restart]]
20 | policy = "always"
21 | retries = 10
22 |
23 | [[vm]]
24 | memory = '512mb'
25 | cpu_kind = 'shared'
26 | cpus = 1
27 |
--------------------------------------------------------------------------------
/packages/deploy/src/supabase/generate-password.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Generate a random password with a length of 16 characters.
3 | */
4 | export function generatePassword(): string {
5 | const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
6 | const length = 16
7 | const randomValues = new Uint8Array(length)
8 | crypto.getRandomValues(randomValues)
9 |
10 | return Array.from(randomValues)
11 | .map((value) => charset[value % charset.length])
12 | .join('')
13 | }
14 |
--------------------------------------------------------------------------------
/apps/web/components/schema/graph.tsx:
--------------------------------------------------------------------------------
1 | import { ReactFlowProvider } from 'reactflow'
2 | import 'reactflow/dist/style.css'
3 |
4 | import TablesGraph from './table-graph'
5 |
6 | export type SchemaGraphProps = {
7 | databaseId: string
8 | schemas: string[]
9 | }
10 |
11 | export default function SchemaGraph({ databaseId, schemas }: SchemaGraphProps) {
12 | return (
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/apps/web/components/layout/header/menu-button.tsx:
--------------------------------------------------------------------------------
1 | import { MenuIcon } from 'lucide-react'
2 | import { useApp } from '~/components/app-provider'
3 | import Sidebar from '~/components/sidebar'
4 | import { Button } from '~/components/ui/button'
5 |
6 | export function MenuButton() {
7 | const { showSidebar, setShowSidebar } = useApp()
8 |
9 | return (
10 | <>
11 | setShowSidebar(!showSidebar)}>
12 |
13 |
14 | >
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/apps/web/lib/embed/worker.ts:
--------------------------------------------------------------------------------
1 | import { FeatureExtractionPipelineOptions, pipeline } from '@xenova/transformers'
2 | import * as Comlink from 'comlink'
3 |
4 | const embedPromise = pipeline('feature-extraction', 'supabase/gte-small', {
5 | quantized: true,
6 | })
7 |
8 | export async function embed(
9 | texts: string[],
10 | options?: FeatureExtractionPipelineOptions
11 | ): Promise {
12 | const embedFn = await embedPromise
13 | const tensor = await embedFn(texts, options)
14 | return tensor.tolist()
15 | }
16 |
17 | Comlink.expose(embed)
18 |
--------------------------------------------------------------------------------
/apps/web/components/sign-in-button.tsx:
--------------------------------------------------------------------------------
1 | import GitHubIcon from '~/assets/github-icon'
2 | import { useApp } from './app-provider'
3 | import { Button } from './ui/button'
4 |
5 | export default function SignInButton() {
6 | const { signIn } = useApp()
7 | return (
8 | {
11 | await signIn()
12 | }}
13 | >
14 |
15 | Sign in with GitHub
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/supabase/functions/certificate/env.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'https://deno.land/x/zod@v3.23.8/mod.ts'
2 |
3 | export const env = z
4 | .object({
5 | ACME_DIRECTORY_URL: z.string(),
6 | ACME_EMAIL: z.string(),
7 | AWS_ACCESS_KEY_ID: z.string(),
8 | AWS_ENDPOINT_URL_S3: z.string(),
9 | AWS_S3_BUCKET: z.string(),
10 | AWS_SECRET_ACCESS_KEY: z.string(),
11 | AWS_REGION: z.string(),
12 | CLOUDFLARE_API_TOKEN: z.string(),
13 | SUPABASE_SERVICE_ROLE_KEY: z.string(),
14 | SUPABASE_URL: z.string(),
15 | })
16 | .parse(Deno.env.toObject())
17 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enablePaths": ["supabase/functions"],
3 | "deno.lint": true,
4 | "deno.unstable": true,
5 | "[javascript]": {
6 | "editor.defaultFormatter": "esbenp.prettier-vscode"
7 | },
8 | "[json]": {
9 | "editor.defaultFormatter": "esbenp.prettier-vscode"
10 | },
11 | "[jsonc]": {
12 | "editor.defaultFormatter": "esbenp.prettier-vscode"
13 | },
14 | "[typescript]": {
15 | "editor.defaultFormatter": "esbenp.prettier-vscode"
16 | },
17 | "[typescriptreact]": {
18 | "editor.defaultFormatter": "esbenp.prettier-vscode"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/apps/web/components/layout/header/bring-your-own-llm-button.tsx:
--------------------------------------------------------------------------------
1 | import { BrainIcon } from 'lucide-react'
2 | import { useApp } from '~/components/app-provider'
3 | import { Button } from '~/components/ui/button'
4 |
5 | export function BringYourOwnLLMButton() {
6 | const { modelProvider } = useApp()
7 |
8 | const modelName = modelProvider.state?.model.split('/').at(-1)
9 | const text = modelProvider.state?.enabled ? modelName : 'Bring your own LLM'
10 |
11 | return (
12 |
13 | {text}
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/apps/web/vite.config.mts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from 'node:url'
2 | import { defineConfig } from 'vite'
3 |
4 | export default defineConfig({
5 | define: {
6 | 'process.env': {},
7 | },
8 | resolve: {
9 | alias: {
10 | '~': fileURLToPath(new URL('.', import.meta.url)),
11 | },
12 | },
13 | build: {
14 | lib: {
15 | entry: fileURLToPath(new URL('sw.ts', import.meta.url)),
16 | fileName: 'sw',
17 | formats: ['es'],
18 | },
19 | outDir: fileURLToPath(new URL('public', import.meta.url)),
20 | emptyOutDir: false,
21 | copyPublicDir: false,
22 | },
23 | })
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | .pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | coverage
11 |
12 | # next.js
13 | .next/
14 | out/
15 |
16 | # production
17 | build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 | *.srl
23 |
24 | # debug
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
29 | # local env files
30 | .env*.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
39 | dbs/
40 | tls/
41 | dist/
42 |
43 | .env
44 | .turbo
45 |
--------------------------------------------------------------------------------
/apps/browser-proxy/src/servername.ts:
--------------------------------------------------------------------------------
1 | const WILDCARD_DOMAIN = process.env.WILDCARD_DOMAIN ?? 'browser.db.build'
2 |
3 | // Escape any dots in the domain since dots are special characters in regex
4 | const escapedDomain = WILDCARD_DOMAIN.replace(/\./g, '\\.')
5 |
6 | // Create the regex pattern dynamically
7 | const regexPattern = new RegExp(`^([^.]+)\\.${escapedDomain}$`)
8 |
9 | export function extractDatabaseId(servername: string): string {
10 | const match = servername.match(regexPattern)
11 | return match![1]!
12 | }
13 |
14 | export function isValidServername(servername: string): boolean {
15 | return regexPattern.test(servername)
16 | }
17 |
--------------------------------------------------------------------------------
/apps/web/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/apps/web/components/layout/header/create-database-button.tsx:
--------------------------------------------------------------------------------
1 | import { PackagePlusIcon } from 'lucide-react'
2 | import Link from 'next/link'
3 | import { Button } from '~/components/ui/button'
4 | import { Tooltip, TooltipContent, TooltipTrigger } from '~/components/ui/tooltip'
5 |
6 | export function CreateDatabaseButton() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | New database
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/supabase/migrations/20240909155000_expose_functions_certificate_secrets.sql:
--------------------------------------------------------------------------------
1 | create function supabase_url()
2 | returns text
3 | language plpgsql
4 | security definer
5 | as $$
6 | declare
7 | secret_value text;
8 | begin
9 | select decrypted_secret into secret_value from vault.decrypted_secrets where name = 'supabase_url';
10 | return secret_value;
11 | end;
12 | $$;
13 |
14 | create function supabase_functions_certificate_secret()
15 | returns text
16 | language plpgsql
17 | security definer
18 | as $$
19 | declare
20 | secret_value text;
21 | begin
22 | select decrypted_secret into secret_value from vault.decrypted_secrets where name = 'supabase_functions_certificate_secret';
23 | return secret_value;
24 | end;
25 | $$;
--------------------------------------------------------------------------------
/apps/web/data/databases/databases-query.ts:
--------------------------------------------------------------------------------
1 | import { UseQueryOptions, useQuery } from '@tanstack/react-query'
2 | import { useApp } from '~/components/app-provider'
3 | import { Database } from '~/lib/db'
4 |
5 | export const useDatabasesQuery = (
6 | options: Omit, 'queryKey' | 'queryFn'> = {}
7 | ) => {
8 | const { dbManager } = useApp()
9 |
10 | return useQuery({
11 | ...options,
12 | queryKey: getDatabasesQueryKey(),
13 | queryFn: async () => {
14 | if (!dbManager) {
15 | throw new Error('dbManager is not available')
16 | }
17 | return await dbManager.getDatabases()
18 | },
19 | staleTime: Infinity,
20 | })
21 | }
22 |
23 | export const getDatabasesQueryKey = () => ['databases']
24 |
--------------------------------------------------------------------------------
/apps/web/lib/llm-provider.ts:
--------------------------------------------------------------------------------
1 | const providerUrlMap = new Map([
2 | ['openai', 'https://api.openai.com/v1'],
3 | ['x-ai', 'https://api.x.ai/v1'],
4 | ['openrouter', 'https://openrouter.ai/api/v1'],
5 | ] as const)
6 |
7 | type MapKeys = T extends Map ? K : never
8 |
9 | export type Provider = MapKeys
10 |
11 | export function getProviderUrl(provider: Provider) {
12 | const url = providerUrlMap.get(provider)
13 |
14 | if (!url) {
15 | throw new Error(`unknown provider: ${provider}`)
16 | }
17 |
18 | return url
19 | }
20 |
21 | export function getProviderId(apiUrl: string): Provider | undefined {
22 | for (const [key, value] of providerUrlMap.entries()) {
23 | if (value === apiUrl) {
24 | return key
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/apps/web/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "~/lib/utils"
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/apps/web/data/databases/database-query.ts:
--------------------------------------------------------------------------------
1 | import { UseQueryOptions, useQuery } from '@tanstack/react-query'
2 | import { useApp } from '~/components/app-provider'
3 | import { Database } from '~/lib/db'
4 |
5 | export const useDatabaseQuery = (
6 | id: string,
7 | options: Omit, 'queryKey' | 'queryFn'> = {}
8 | ) => {
9 | const { dbManager } = useApp()
10 | return useQuery({
11 | ...options,
12 | queryKey: getDatabaseQueryKey(id),
13 | queryFn: async () => {
14 | if (!dbManager) {
15 | throw new Error('dbManager is not available')
16 | }
17 | return await dbManager.getDatabase(id)
18 | },
19 | staleTime: Infinity,
20 | })
21 | }
22 |
23 | export const getDatabaseQueryKey = (id: string) => ['database', id]
24 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext", "webworker"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "target": "ES2015",
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "~/*": ["./*"]
23 | }
24 | },
25 | "include": [
26 | "next-env.d.ts",
27 | "global.d.ts",
28 | "**/*.ts",
29 | "**/*.tsx",
30 | ".next/types/**/*.ts",
31 | "vite.config.mts"
32 | ],
33 | "exclude": ["node_modules"]
34 | }
35 |
--------------------------------------------------------------------------------
/apps/web/data/messages/messages-query.ts:
--------------------------------------------------------------------------------
1 | import { UseQueryOptions, useQuery } from '@tanstack/react-query'
2 | import { Message } from 'ai'
3 | import { useApp } from '~/components/app-provider'
4 |
5 | export const useMessagesQuery = (
6 | databaseId: string,
7 | options: Omit, 'queryKey' | 'queryFn'> = {}
8 | ) => {
9 | const { dbManager } = useApp()
10 |
11 | return useQuery({
12 | ...options,
13 | queryKey: getMessagesQueryKey(databaseId),
14 | queryFn: async () => {
15 | if (!dbManager) {
16 | throw new Error('dbManager is not available')
17 | }
18 | return await dbManager.getMessages(databaseId)
19 | },
20 | staleTime: Infinity,
21 | })
22 | }
23 |
24 | export const getMessagesQueryKey = (databaseId: string) => ['messages', { databaseId }]
25 |
--------------------------------------------------------------------------------
/apps/browser-proxy/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@database.build/browser-proxy",
3 | "type": "module",
4 | "scripts": {
5 | "start": "node --env-file=.env --experimental-strip-types src/index.ts",
6 | "dev": "node --watch --env-file=.env --experimental-strip-types src/index.ts",
7 | "type-check": "tsc"
8 | },
9 | "dependencies": {
10 | "@aws-sdk/client-s3": "^3.645.0",
11 | "@supabase/supabase-js": "^2.45.4",
12 | "debug": "^4.3.7",
13 | "expiry-map": "^2.0.0",
14 | "findhit-proxywrap": "^0.3.13",
15 | "nanoid": "^5.0.7",
16 | "p-memoize": "^7.1.1",
17 | "pg-gateway": "^0.3.0-beta.3",
18 | "ws": "^8.18.0"
19 | },
20 | "devDependencies": {
21 | "@total-typescript/tsconfig": "^1.0.4",
22 | "@types/debug": "^4.1.12",
23 | "@types/node": "^22.5.4",
24 | "typescript": "^5.5.4"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/apps/web/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "~/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/apps/web/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import './globals.css'
2 | import 'katex/dist/katex.min.css'
3 |
4 | import type { Metadata } from 'next'
5 | import { Inter as FontSans } from 'next/font/google'
6 | import Providers from '~/components/providers'
7 | import { cn } from '~/lib/utils'
8 |
9 | const fontSans = FontSans({
10 | subsets: ['latin'],
11 | variable: '--font-sans',
12 | })
13 |
14 | export const metadata: Metadata = {
15 | title: 'Postgres Sandbox',
16 | description: 'In-browser Postgres sandbox with AI assistance',
17 | }
18 |
19 | export default function RootLayout({
20 | children,
21 | }: Readonly<{
22 | children: React.ReactNode
23 | }>) {
24 | return (
25 |
26 |
27 | {children}
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/.github/workflows/run-migrations.yml:
--------------------------------------------------------------------------------
1 | name: Run Migrations
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | run-migrations:
10 | runs-on: ubuntu-latest
11 |
12 | env:
13 | SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
14 | SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_PROD_DB_PASSWORD }}
15 | PROJECT_ID: ${{ vars.SUPABASE_PROD_PROJECT_ID }}
16 |
17 | # Create placeholder vars so that supabase/config.toml can load
18 | SUPABASE_AUTH_GITHUB_CLIENT_ID: placeholder
19 | SUPABASE_AUTH_GITHUB_SECRET: placeholder
20 | SUPABASE_AUTH_GITHUB_REDIRECT_URI: placeholder
21 |
22 | steps:
23 | - uses: actions/checkout@v4
24 | - uses: supabase/setup-cli@v1
25 | with:
26 | version: latest
27 | - run: supabase link --project-ref $PROJECT_ID
28 | - run: supabase db push
29 |
--------------------------------------------------------------------------------
/apps/web/lib/websocket-protocol.ts:
--------------------------------------------------------------------------------
1 | // Our protocol structure:
2 | // +------------------+-----------------------------+
3 | // | connectionId | message |
4 | // | (16 bytes) | (variable length) |
5 | // +------------------+-----------------------------+
6 |
7 | export function parse(data: Uint8Array) {
8 | const connectionIdBytes = data.subarray(0, 16)
9 | const connectionId = new TextDecoder().decode(connectionIdBytes)
10 | const message = data.subarray(16)
11 | return { connectionId, message }
12 | }
13 |
14 | export function serialize(connectionId: string, message: Uint8Array) {
15 | const encoder = new TextEncoder()
16 | const connectionIdBytes = encoder.encode(connectionId)
17 | const data = new Uint8Array(connectionIdBytes.length + message.length)
18 | data.set(connectionIdBytes, 0)
19 | data.set(message, connectionIdBytes.length)
20 | return data
21 | }
22 |
--------------------------------------------------------------------------------
/apps/browser-proxy/README.md:
--------------------------------------------------------------------------------
1 | # Browser Proxy
2 |
3 | This app is a proxy that sits between the browser and a PostgreSQL client.
4 |
5 | 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.
6 |
7 | ## Development
8 |
9 | Copy the `.env.example` file to `.env` and set the correct environment variables.
10 |
11 | Run the dev server from the monorepo root. See [Development](../../README.md#development).
12 |
13 | The browser proxy will be listening on ports `5432` (Postgres TCP) and `443` (Web Sockets).
14 |
15 | ## Deployment
16 |
17 | Create a new app on Fly.io, for example `database-build-browser-proxy`.
18 |
19 | Fill the app's secrets with the correct environment variables based on the `.env.example` file.
20 |
21 | Deploy the app:
22 |
23 | ```sh
24 | fly deploy --app database-build-browser-proxy
25 | ```
26 |
--------------------------------------------------------------------------------
/apps/web/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_SUPABASE_ANON_KEY=""
2 | NEXT_PUBLIC_SUPABASE_URL=""
3 | NEXT_PUBLIC_BROWSER_PROXY_DOMAIN="browser.dev.db.build"
4 | NEXT_PUBLIC_DEPLOY_WORKER_DOMAIN="http://localhost:4000"
5 | NEXT_PUBLIC_SUPABASE_OAUTH_CLIENT_ID=""
6 | NEXT_PUBLIC_SUPABASE_PLATFORM_URL=https://supabase.com
7 | NEXT_PUBLIC_SUPABASE_PLATFORM_API_URL=https://api.supabase.com
8 |
9 | OPENAI_API_KEY=""
10 | # Optional
11 | # OPENAI_API_BASE=""
12 | # OPENAI_MODEL=""
13 |
14 | # Vercel KV (local Docker available)
15 | KV_REST_API_URL="http://localhost:8080"
16 | KV_REST_API_TOKEN="local_token"
17 |
18 | SUPABASE_OAUTH_SECRET=""
19 | SUPABASE_SERVICE_ROLE_KEY=""
20 |
21 | # Optional
22 | #LOGFLARE_SOURCE=""
23 | #LOGFLARE_API_KEY=""
24 |
--------------------------------------------------------------------------------
/apps/browser-proxy/src/protocol.ts:
--------------------------------------------------------------------------------
1 | import { customAlphabet } from 'nanoid'
2 |
3 | const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 16)
4 |
5 | export function getConnectionId(): string {
6 | return nanoid()
7 | }
8 |
9 | export function parse(data: T) {
10 | const connectionIdBytes = data.subarray(0, 16)
11 | const connectionId = new TextDecoder().decode(connectionIdBytes)
12 | const message = data.subarray(16)
13 | return { connectionId, message } as { connectionId: string; message: T }
14 | }
15 |
16 | export function serialize(connectionId: string, message: Uint8Array) {
17 | const encoder = new TextEncoder()
18 | const connectionIdBytes = encoder.encode(connectionId)
19 | const data = new Uint8Array(connectionIdBytes.length + message.length)
20 | data.set(connectionIdBytes, 0)
21 | data.set(message, connectionIdBytes.length)
22 | return data
23 | }
24 |
--------------------------------------------------------------------------------
/apps/web/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "~/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/apps/web/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "~/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/apps/web/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "~/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/packages/deploy/src/supabase/management-api/client.ts:
--------------------------------------------------------------------------------
1 | import createClient, { type Middleware } from 'openapi-fetch'
2 | import type { paths } from './types.js'
3 | import { IntegrationRevokedError } from '../../error.js'
4 | import type { SupabasePlatformConfig } from '../types.js'
5 |
6 | const integrationRevokedMiddleware: Middleware = {
7 | async onResponse({ response }) {
8 | if (response.status === 406) {
9 | throw new IntegrationRevokedError()
10 | }
11 | },
12 | }
13 |
14 | export function createManagementApiClient(
15 | ctx: { supabasePlatformConfig: SupabasePlatformConfig },
16 | accessToken: string
17 | ) {
18 | const client = createClient({
19 | baseUrl: ctx.supabasePlatformConfig.apiUrl,
20 | headers: {
21 | 'Content-Type': 'application/json',
22 | Authorization: `Bearer ${accessToken}`,
23 | },
24 | })
25 |
26 | client.use(integrationRevokedMiddleware)
27 |
28 | return client
29 | }
30 |
--------------------------------------------------------------------------------
/apps/web/data/merged-databases/merged-databases.ts:
--------------------------------------------------------------------------------
1 | import { useDatabasesQuery } from '../databases/databases-query'
2 | import { useDeployedDatabasesQuery } from '../deployed-databases/deployed-databases-query'
3 |
4 | /**
5 | * Merges local databases with remote deployed databases.
6 | */
7 | export function useMergedDatabases() {
8 | const { data: localDatabases, isLoading: isLoadingLocalDatabases } = useDatabasesQuery()
9 | const { data: deployedDatabases, isLoading: isLoadingDeployedDatabases } =
10 | useDeployedDatabasesQuery()
11 |
12 | const isLoading = isLoadingLocalDatabases && isLoadingDeployedDatabases
13 |
14 | if (!localDatabases) {
15 | return { data: undefined, isLoading }
16 | }
17 |
18 | const databases = localDatabases.map((db) => ({
19 | ...db,
20 | deployments:
21 | deployedDatabases?.filter((deployedDb) => deployedDb.local_database_id === db.id) ?? [],
22 | }))
23 |
24 | return { data: databases, isLoading }
25 | }
26 |
--------------------------------------------------------------------------------
/apps/web/lib/embed/index.ts:
--------------------------------------------------------------------------------
1 | import { FeatureExtractionPipelineOptions } from '@xenova/transformers'
2 | import * as Comlink from 'comlink'
3 |
4 | type EmbedFn = (typeof import('./worker.ts'))['embed']
5 |
6 | let embedFn: EmbedFn
7 |
8 | // Wrap embed function in WebWorker via comlink
9 | function getEmbedFn() {
10 | if (embedFn) {
11 | return embedFn
12 | }
13 |
14 | if (typeof window === 'undefined') {
15 | throw new Error('Embed function only available in the browser')
16 | }
17 |
18 | const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' })
19 | embedFn = Comlink.wrap(worker)
20 | return embedFn
21 | }
22 |
23 | /**
24 | * Generates an embedding for each text in `texts`.
25 | *
26 | * @returns An array of vectors.
27 | */
28 | export async function embed(texts: string[], options?: FeatureExtractionPipelineOptions) {
29 | const embedFn = getEmbedFn()
30 | return await embedFn(texts, options)
31 | }
32 |
--------------------------------------------------------------------------------
/apps/web/components/deploy/deploy-failure-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui/dialog'
4 | import { SupabaseIcon } from '../supabase-icon'
5 |
6 | export type DeployFailureDialogProps = {
7 | open: boolean
8 | onOpenChange: (open: boolean) => void
9 | errorMessage: string
10 | }
11 |
12 | export function DeployFailureDialog({
13 | open,
14 | onOpenChange,
15 | errorMessage,
16 | }: DeployFailureDialogProps) {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 | Database deployment failed
24 |
25 |
26 |
27 | {errorMessage}
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/apps/web/components/tools/csv-import.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import { formatSql } from '~/lib/sql-util'
3 | import { ToolInvocation } from '~/lib/tools'
4 | import CodeAccordion from '../code-accordion'
5 |
6 | export type CsvExportProps = {
7 | toolInvocation: ToolInvocation<'importCsv'>
8 | }
9 |
10 | export default function CsvImport({ toolInvocation }: CsvExportProps) {
11 | const { sql } = toolInvocation.args
12 |
13 | const formattedSql = useMemo(() => formatSql(sql), [sql])
14 |
15 | if (!('result' in toolInvocation)) {
16 | return null
17 | }
18 |
19 | const { result } = toolInvocation
20 |
21 | if (!result.success) {
22 | return (
23 |
29 | )
30 | }
31 |
32 | return
33 | }
34 |
--------------------------------------------------------------------------------
/apps/web/components/tools/sql-import.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import { formatSql } from '~/lib/sql-util'
3 | import { ToolInvocation } from '~/lib/tools'
4 | import CodeAccordion from '../code-accordion'
5 |
6 | export type SqlImportProps = {
7 | toolInvocation: ToolInvocation<'importSql'>
8 | }
9 |
10 | export default function SqlImport({ toolInvocation }: SqlImportProps) {
11 | const { fileId, sql } = toolInvocation.args
12 |
13 | const formattedSql = useMemo(() => formatSql(sql), [sql])
14 |
15 | if (!('result' in toolInvocation)) {
16 | return null
17 | }
18 |
19 | const { result } = toolInvocation
20 |
21 | if (!result.success) {
22 | return (
23 |
29 | )
30 | }
31 |
32 | return
33 | }
34 |
--------------------------------------------------------------------------------
/apps/browser-proxy/src/index.ts:
--------------------------------------------------------------------------------
1 | import { httpsServer } from './websocket-server.ts'
2 | import { tcpServer } from './tcp-server.ts'
3 |
4 | process.on('unhandledRejection', (reason, promise) => {
5 | console.error({ location: 'unhandledRejection', reason, promise })
6 | })
7 |
8 | process.on('uncaughtException', (error) => {
9 | console.error({ location: 'uncaughtException', error })
10 | })
11 |
12 | httpsServer.listen(443, () => {
13 | console.log('websocket server listening on port 443')
14 | })
15 |
16 | tcpServer.listen(5432, () => {
17 | console.log('tcp server listening on port 5432')
18 | })
19 |
20 | const shutdown = async () => {
21 | await Promise.allSettled([
22 | new Promise((res) =>
23 | httpsServer.close(() => {
24 | res()
25 | })
26 | ),
27 | new Promise((res) =>
28 | tcpServer.close(() => {
29 | res()
30 | })
31 | ),
32 | ])
33 | process.exit(0)
34 | }
35 |
36 | process.on('SIGTERM', shutdown)
37 | process.on('SIGINT', shutdown)
38 |
--------------------------------------------------------------------------------
/packages/deploy/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@database.build/deploy",
3 | "version": "0.0.0",
4 | "description": "Database deployment utilities",
5 | "private": true,
6 | "type": "module",
7 | "exports": {
8 | ".": "./dist/index.js",
9 | "./supabase": "./dist/supabase/index.js"
10 | },
11 | "scripts": {
12 | "type-check": "tsc",
13 | "build": "tsup",
14 | "clean": "rm -rf dist",
15 | "generate:database-types": "supabase gen types --lang=typescript --local > src/supabase/database-types.ts",
16 | "generate:management-api-types": "openapi-typescript https://api.supabase.com/api/v1-json -o ./src/supabase/management-api/types.ts"
17 | },
18 | "dependencies": {
19 | "@supabase/supabase-js": "^2.46.1",
20 | "neverthrow": "^8.0.0",
21 | "openapi-fetch": "0.13.1",
22 | "zod": "^3.23.8"
23 | },
24 | "devDependencies": {
25 | "@total-typescript/tsconfig": "^1.0.4",
26 | "openapi-typescript": "^7.4.2",
27 | "tsup": "^8.3.5",
28 | "typescript": "^5.6.3"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/apps/deploy-worker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:22-alpine AS base
2 |
3 | FROM base AS builder
4 |
5 | WORKDIR /app
6 |
7 | RUN npm install -g turbo@^2
8 | COPY . .
9 |
10 | # Generate a partial monorepo with a pruned lockfile for a target workspace.
11 | RUN turbo prune @database.build/deploy-worker --docker
12 |
13 | FROM base AS installer
14 | WORKDIR /app
15 |
16 | # First install the dependencies (as they change less often)
17 | COPY --from=builder /app/out/json/ .
18 | RUN npm install
19 |
20 | # Build the project
21 | COPY --from=builder /app/out/full/ .
22 | RUN npx turbo run build --filter=@database.build/deploy-worker
23 |
24 | FROM base AS runner
25 |
26 | RUN apk add --no-cache postgresql16-client
27 |
28 | # Don't run production as root
29 | RUN addgroup --system --gid 1001 nodejs
30 | RUN adduser --system --uid 1001 nodejs
31 | USER nodejs
32 |
33 | COPY --from=installer --chown=nodejs:nodejs /app /app
34 |
35 | WORKDIR /app/apps/deploy-worker
36 |
37 | EXPOSE 443
38 | EXPOSE 5432
39 |
40 | CMD ["node", "--experimental-strip-types", "src/index.ts"]
--------------------------------------------------------------------------------
/apps/web/components/live-share-icon.tsx:
--------------------------------------------------------------------------------
1 | import { cx } from 'class-variance-authority'
2 |
3 | export const LiveShareIcon = (props: { size?: number; className?: string }) => (
4 |
13 |
18 |
19 | )
20 |
--------------------------------------------------------------------------------
/apps/web/data/deploy-waitlist/deploy-waitlist-create-mutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
2 | import { createClient } from '~/utils/supabase/client'
3 | import { getIsOnDeployWaitlistQueryKey } from './deploy-waitlist-query'
4 |
5 | export const useDeployWaitlistCreateMutation = ({
6 | onSuccess,
7 | onError,
8 | ...options
9 | }: Omit, 'mutationFn'> = {}) => {
10 | const queryClient = useQueryClient()
11 |
12 | return useMutation({
13 | mutationFn: async () => {
14 | const supabase = createClient()
15 |
16 | const { error } = await supabase.from('deploy_waitlist').insert({})
17 |
18 | if (error) {
19 | throw error
20 | }
21 | },
22 | async onSuccess(data, variables, context) {
23 | await Promise.all([
24 | queryClient.invalidateQueries({ queryKey: getIsOnDeployWaitlistQueryKey() }),
25 | ])
26 | return onSuccess?.(data, variables, context)
27 | },
28 | ...options,
29 | })
30 | }
31 |
--------------------------------------------------------------------------------
/apps/deploy-worker/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@database.build/deploy-worker",
3 | "type": "module",
4 | "scripts": {
5 | "start": "node --env-file=.env --experimental-strip-types src/index.ts",
6 | "dev": "npm run start",
7 | "build": "echo 'built'",
8 | "type-check": "tsc",
9 | "generate:database-types": "npx supabase gen types --lang=typescript --local > ./supabase/database-types.ts",
10 | "generate:management-api-types": "npx openapi-typescript https://api.supabase.com/api/v1-json -o ./supabase/management-api/types.ts"
11 | },
12 | "dependencies": {
13 | "@database.build/deploy": "*",
14 | "@hono/node-server": "^1.13.2",
15 | "@hono/zod-validator": "^0.4.1",
16 | "@supabase/supabase-js": "^2.45.4",
17 | "hono": "^4.6.5",
18 | "neverthrow": "^8.0.0",
19 | "openapi-fetch": "^0.13.0",
20 | "zod": "^3.23.8"
21 | },
22 | "devDependencies": {
23 | "@total-typescript/tsconfig": "^1.0.4",
24 | "@types/node": "^22.5.4",
25 | "openapi-typescript": "^7.4.2",
26 | "typescript": "^5.5.4"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/supabase/migrations/20240806202109_publish_waitlist.sql:
--------------------------------------------------------------------------------
1 | create table
2 | public.publish_waitlist (
3 | id bigint primary key generated always as identity,
4 | user_id uuid not null default auth.uid () references auth.users (id) on delete cascade,
5 | created_at timestamp
6 | with
7 | time zone not null default now ()
8 | );
9 |
10 | alter table public.publish_waitlist enable row level security;
11 |
12 | create policy "Users can only see their own waitlist record." on public.publish_waitlist for
13 | select
14 | to authenticated using (
15 | user_id = (
16 | select
17 | auth.uid ()
18 | )
19 | );
20 |
21 | create policy "Users can only insert their own waitlist record." on public.publish_waitlist for insert to authenticated
22 | with
23 | check (
24 | user_id = (
25 | select
26 | auth.uid ()
27 | )
28 | );
29 |
30 | create policy "Users can only delete their own waitlist record." on public.publish_waitlist for delete to authenticated using (
31 | user_id = (
32 | select
33 | auth.uid ()
34 | )
35 | );
--------------------------------------------------------------------------------
/apps/web/data/deploy-waitlist/deploy-waitlist-query.ts:
--------------------------------------------------------------------------------
1 | import { UseQueryOptions, useQuery } from '@tanstack/react-query'
2 | import { createClient } from '~/utils/supabase/client'
3 |
4 | export const useIsOnDeployWaitlistQuery = (
5 | options: Omit, 'queryKey' | 'queryFn'> = {}
6 | ) => {
7 | const supabase = createClient()
8 |
9 | return useQuery({
10 | ...options,
11 | queryKey: getIsOnDeployWaitlistQueryKey(),
12 | queryFn: async () => {
13 | const { data, error } = await supabase.auth.getUser()
14 | if (error) {
15 | throw error
16 | }
17 | const { user } = data
18 |
19 | const { data: waitlistRecord, error: waitlistError } = await supabase
20 | .from('deploy_waitlist')
21 | .select('*')
22 | .eq('user_id', user.id)
23 | .maybeSingle()
24 |
25 | if (waitlistError) {
26 | throw waitlistError
27 | }
28 |
29 | return waitlistRecord !== null
30 | },
31 | })
32 | }
33 |
34 | export const getIsOnDeployWaitlistQueryKey = () => ['deploy-waitlist']
35 |
--------------------------------------------------------------------------------
/packages/deploy/src/supabase/revoke-integration.ts:
--------------------------------------------------------------------------------
1 | import type { SupabaseClient } from './types.js'
2 |
3 | export async function revokeIntegration(
4 | ctx: {
5 | supabase: SupabaseClient
6 | supabaseAdmin: SupabaseClient
7 | },
8 | params: { integrationId: number }
9 | ) {
10 | const integration = await ctx.supabase
11 | .from('deployment_provider_integrations')
12 | .select('*')
13 | .eq('id', params.integrationId)
14 | .single()
15 |
16 | if (integration.error) {
17 | throw new Error('Integration not found')
18 | }
19 |
20 | const updatedIntegration = await ctx.supabase
21 | .from('deployment_provider_integrations')
22 | .update({ revoked_at: 'now', credentials: null })
23 | .eq('id', params.integrationId)
24 |
25 | if (updatedIntegration.error) {
26 | throw new Error('Failed to revoke integration')
27 | }
28 |
29 | const deleteSecret = await ctx.supabaseAdmin.rpc('delete_secret', {
30 | secret_id: integration.data.credentials!,
31 | })
32 |
33 | if (deleteSecret.error) {
34 | throw new Error('Failed to delete the integration credentials')
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/apps/web/data/deployed-databases/deployed-databases-query.ts:
--------------------------------------------------------------------------------
1 | import { UseQueryOptions, useQuery } from '@tanstack/react-query'
2 | import { createClient } from '~/utils/supabase/client'
3 |
4 | export type DeployedDatabase = Awaited>[number]
5 |
6 | async function getDeployedDatabases() {
7 | const supabase = createClient()
8 | const { data, error } = await supabase
9 | .from('latest_deployed_databases')
10 | .select(
11 | '*, ...deployment_provider_integrations!inner(...deployment_providers!inner(provider_name:name))'
12 | )
13 |
14 | if (error) {
15 | throw error
16 | }
17 |
18 | return data
19 | }
20 |
21 | export const useDeployedDatabasesQuery = (
22 | options: Omit, 'queryKey' | 'queryFn'> = {}
23 | ) => {
24 | return useQuery({
25 | ...options,
26 | queryKey: getDeployedDatabasesQueryKey(),
27 | queryFn: async () => {
28 | return await getDeployedDatabases()
29 | },
30 | })
31 | }
32 |
33 | export const getDeployedDatabasesQueryKey = () => ['deployed-databases', 'authenticated']
34 |
--------------------------------------------------------------------------------
/apps/web/assets/github-icon.tsx:
--------------------------------------------------------------------------------
1 | import { SVGProps } from 'react'
2 |
3 | const GitHubIcon = (props: SVGProps) => (
4 |
5 |
11 |
12 | )
13 | export default GitHubIcon
14 |
--------------------------------------------------------------------------------
/apps/web/data/databases/database-create-mutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
2 | import { useApp } from '~/components/app-provider'
3 | import { Database } from '~/lib/db'
4 | import { getDatabasesQueryKey } from './databases-query'
5 |
6 | export type DatabaseCreateVariables = {
7 | id: string
8 | isHidden: boolean
9 | }
10 |
11 | export const useDatabaseCreateMutation = ({
12 | onSuccess,
13 | onError,
14 | ...options
15 | }: Omit, 'mutationFn'> = {}) => {
16 | const { dbManager } = useApp()
17 | const queryClient = useQueryClient()
18 |
19 | return useMutation({
20 | mutationFn: async ({ id, isHidden }) => {
21 | if (!dbManager) {
22 | throw new Error('dbManager is not available')
23 | }
24 | return await dbManager.createDatabase(id, { isHidden })
25 | },
26 | async onSuccess(data, variables, context) {
27 | await Promise.all([queryClient.invalidateQueries({ queryKey: getDatabasesQueryKey() })])
28 | return onSuccess?.(data, variables, context)
29 | },
30 | ...options,
31 | })
32 | }
33 |
--------------------------------------------------------------------------------
/apps/web/components/tools/generated-embedding.tsx:
--------------------------------------------------------------------------------
1 | import { codeBlock } from 'common-tags'
2 | import { ToolInvocation } from '~/lib/tools'
3 | import MarkdownAccordion from '../markdown-accordion'
4 |
5 | export type GeneratedEmbeddingProps = {
6 | toolInvocation: ToolInvocation<'embed'>
7 | }
8 |
9 | export default function GeneratedEmbedding({ toolInvocation }: GeneratedEmbeddingProps) {
10 | const { texts } = toolInvocation.args
11 |
12 | if (!('result' in toolInvocation)) {
13 | return null
14 | }
15 |
16 | if (!toolInvocation.result.success) {
17 | const content = codeBlock`
18 | ${texts.map((text) => `> ${text}`).join('\n')}
19 | `
20 |
21 | return (
22 |
27 | )
28 | }
29 |
30 | const { ids } = toolInvocation.result
31 |
32 | const content = codeBlock`
33 | Results stored in \`meta.embeddings\` table:
34 |
35 | ${texts.map((text, i) => `- **\`id\`:** ${ids[i]}\n\n **\`content\`:** ${text}`).join('\n')}
36 | `
37 |
38 | return
39 | }
40 |
--------------------------------------------------------------------------------
/apps/web/lib/use-breakpoint.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copied from supabase/supabase common package.
3 | */
4 |
5 | import { useState } from 'react'
6 | import { useIsomorphicLayoutEffect, useWindowSize } from 'react-use'
7 |
8 | /**
9 | * Map of Tailwind default breakpoint values. Allows setting a value by
10 | * Tailwind breakpoint, so that it syncs up with CSS changes.
11 | *
12 | * Note Tailwind uses `min-width` logic, whereas we use `max-width` logic, so
13 | * the values are offset by 1px.
14 | *
15 | * Source:
16 | * https://tailwindcss.com/docs/responsive-design
17 | */
18 | const twBreakpointMap = {
19 | sm: 639,
20 | md: 767,
21 | lg: 1023,
22 | xl: 1027,
23 | '2xl': 1535,
24 | }
25 |
26 | export function useBreakpoint(breakpoint: number | keyof typeof twBreakpointMap = 'lg') {
27 | const [isBreakpoint, setIsBreakpoint] = useState(false)
28 | const { width } = useWindowSize()
29 |
30 | const _breakpoint = typeof breakpoint === 'string' ? twBreakpointMap[breakpoint] : breakpoint
31 |
32 | useIsomorphicLayoutEffect(() => {
33 | if (width <= _breakpoint) {
34 | setIsBreakpoint(true)
35 | } else {
36 | setIsBreakpoint(false)
37 | }
38 | }, [width])
39 |
40 | return isBreakpoint
41 | }
42 |
--------------------------------------------------------------------------------
/apps/web/data/merged-databases/merged-database.ts:
--------------------------------------------------------------------------------
1 | import { Database } from '~/lib/db'
2 | import {
3 | DeployedDatabase,
4 | useDeployedDatabasesQuery,
5 | } from '../deployed-databases/deployed-databases-query'
6 | import { useDatabaseQuery } from '../databases/database-query'
7 |
8 | /**
9 | * A local database with remote deployment information.
10 | */
11 | export type MergedDatabase = Database & {
12 | deployments: DeployedDatabase[]
13 | }
14 |
15 | /**
16 | * Merges local database with remote deployed database.
17 | */
18 | export function useMergedDatabase(id: string) {
19 | const { data: localDatabase, isLoading: isLoadingLocalDatabase } = useDatabaseQuery(id)
20 | const { data: deployedDatabases, isLoading: isLoadingDeployedDatabases } =
21 | useDeployedDatabasesQuery()
22 |
23 | const isLoading = isLoadingLocalDatabase && isLoadingDeployedDatabases
24 |
25 | if (!localDatabase) {
26 | return { data: undefined, isLoading }
27 | }
28 |
29 | const database = {
30 | ...localDatabase,
31 | deployments:
32 | deployedDatabases?.filter(
33 | (deployedDb) => deployedDb.local_database_id === localDatabase.id
34 | ) ?? [],
35 | }
36 |
37 | return { data: database, isLoading }
38 | }
39 |
--------------------------------------------------------------------------------
/apps/web/utils/supabase/server.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from '@supabase/ssr'
2 | import { cookies } from 'next/headers'
3 | import { Database } from './db-types'
4 | import { createClient as createSupabaseClient } from '@supabase/supabase-js'
5 |
6 | export function createClient() {
7 | const cookieStore = cookies()
8 |
9 | return createServerClient(
10 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
11 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
12 | {
13 | cookies: {
14 | getAll() {
15 | return cookieStore.getAll()
16 | },
17 | setAll(cookiesToSet) {
18 | try {
19 | cookiesToSet.forEach(({ name, value, options }) =>
20 | cookieStore.set(name, value, options)
21 | )
22 | } catch {
23 | // The `setAll` method was called from a Server Component.
24 | // This can be ignored if you have middleware refreshing
25 | // user sessions.
26 | }
27 | },
28 | },
29 | }
30 | )
31 | }
32 |
33 | export function createAdminClient() {
34 | return createSupabaseClient(
35 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
36 | process.env.SUPABASE_SERVICE_ROLE_KEY!
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/apps/web/data/messages/message-create-mutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
2 | import { useApp } from '~/components/app-provider'
3 | import { Message } from '~/lib/db'
4 | import { getMessagesQueryKey } from './messages-query'
5 |
6 | export type MessageCreateVariables = {
7 | message: Message
8 | }
9 |
10 | export const useMessageCreateMutation = (
11 | databaseId: string,
12 | {
13 | onSuccess,
14 | onError,
15 | ...options
16 | }: Omit, 'mutationFn'> = {}
17 | ) => {
18 | const { dbManager } = useApp()
19 | const queryClient = useQueryClient()
20 |
21 | return useMutation({
22 | mutationFn: async ({ message }) => {
23 | if (!dbManager) {
24 | throw new Error('dbManager is not available')
25 | }
26 | return await dbManager.createMessage(databaseId, message)
27 | },
28 | async onSuccess(data, variables, context) {
29 | await Promise.all([
30 | queryClient.invalidateQueries({ queryKey: getMessagesQueryKey(databaseId) }),
31 | ])
32 | return onSuccess?.(data, variables, context)
33 | },
34 | ...options,
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/packages/deploy/src/supabase/get-database-url.ts:
--------------------------------------------------------------------------------
1 | import type { SupabaseProviderMetadata } from './types.js'
2 |
3 | /**
4 | * Get the direct database url for a given Supabase project.
5 | */
6 | export function getDatabaseUrl(params: {
7 | project: SupabaseProviderMetadata['project']
8 | databaseUser?: string
9 | databasePassword?: string
10 | }) {
11 | const user = params.databaseUser ?? params.project.database.user
12 | const password = params.databasePassword ?? '[YOUR-PASSWORD]'
13 |
14 | const { database } = params.project
15 |
16 | return `postgresql://${user}:${password}@${database.host}:${database.port}/${database.name}`
17 | }
18 |
19 | /**
20 | * Get the pooler url for a given Supabase project.
21 | */
22 | export function getPoolerUrl(params: {
23 | project: SupabaseProviderMetadata['project']
24 | databaseUser?: string
25 | databasePassword?: string
26 | }) {
27 | const user = params.databaseUser
28 | ? params.project.pooler.user.replace('postgres', params.databaseUser)
29 | : params.project.pooler.user
30 | const password = params.databasePassword ?? '[YOUR-PASSWORD]'
31 |
32 | const { pooler } = params.project
33 |
34 | return `postgresql://${user}:${password}@${pooler.host}:${pooler.port}/${pooler.name}`
35 | }
36 |
--------------------------------------------------------------------------------
/apps/web/components/deploy/deploy-success-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui/dialog'
4 | import { SupabaseDeployInfo, SupabaseDeploymentInfo } from './deploy-info'
5 | import { SupabaseIcon } from '../supabase-icon'
6 |
7 | export type DeploySuccessDialogProps = {
8 | open: boolean
9 | onOpenChange: (open: boolean) => void
10 | deployInfo: SupabaseDeploymentInfo
11 | }
12 |
13 | export function DeploySuccessDialog({ open, onOpenChange, deployInfo }: DeploySuccessDialogProps) {
14 | const isRedeploy = !deployInfo.databasePassword
15 | const deployText = isRedeploy ? 'redeployed' : 'deployed'
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 | Database {deployText}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/apps/web/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "~/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/apps/web/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "~/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/apps/web/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as TooltipPrimitive from '@radix-ui/react-tooltip'
5 |
6 | import { cn } from '~/lib/utils'
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/apps/web/data/databases/database-delete-mutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
2 | import { useApp } from '~/components/app-provider'
3 | import { getDatabaseQueryKey } from './database-query'
4 | import { getDatabasesQueryKey } from './databases-query'
5 |
6 | export type DatabaseDeleteVariables = {
7 | id: string
8 | }
9 |
10 | export const useDatabaseDeleteMutation = ({
11 | onSuccess,
12 | onError,
13 | ...options
14 | }: Omit, 'mutationFn'> = {}) => {
15 | const { dbManager } = useApp()
16 | const queryClient = useQueryClient()
17 |
18 | return useMutation({
19 | mutationFn: async ({ id }) => {
20 | if (!dbManager) {
21 | throw new Error('dbManager is not available')
22 | }
23 | return await dbManager.deleteDatabase(id)
24 | },
25 | async onSuccess(data, variables, context) {
26 | await Promise.all([queryClient.invalidateQueries({ queryKey: getDatabasesQueryKey() })])
27 | await Promise.all([
28 | queryClient.invalidateQueries({ queryKey: getDatabaseQueryKey(variables.id) }),
29 | ])
30 | return onSuccess?.(data, variables, context)
31 | },
32 | ...options,
33 | })
34 | }
35 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.v2.json",
3 | "ui": "stream",
4 | "tasks": {
5 | "@database.build/deploy#build": {
6 | "dependsOn": ["^build"],
7 | "env": [],
8 | "outputs": ["dist/**"],
9 | "cache": true
10 | },
11 | "@database.build/deploy-worker#dev": {
12 | "dependsOn": ["^build"],
13 | "persistent": true,
14 | "interruptible": true,
15 | "cache": false
16 | },
17 | "@database.build/deploy-worker#build": {
18 | "dependsOn": ["^build"],
19 | "env": ["SUPABASE_*"],
20 | "cache": false
21 | },
22 | "@database.build/browser-proxy#dev": {
23 | "dependsOn": ["^build"],
24 | "persistent": true,
25 | "interruptible": true,
26 | "cache": false
27 | },
28 | "@database.build/web#dev": {
29 | "dependsOn": ["^build"],
30 | "outputs": [".next/**", "!.next/cache/**"],
31 | "persistent": true,
32 | "cache": true
33 | },
34 | "@database.build/web#build": {
35 | "dependsOn": ["^build"],
36 | "env": ["NEXT_PUBLIC_*", "OPENAI_*", "KV_*", "SUPABASE_*", "LOGFLARE_*"],
37 | "outputs": [".next/**", "!.next/cache/**"],
38 | "cache": true
39 | },
40 | "type-check": {
41 | "dependsOn": ["^type-check"]
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/apps/web/components/deploy/integration-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { DialogProps } from '@radix-ui/react-dialog'
4 | import { Button } from '~/components/ui/button'
5 | import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui/dialog'
6 | import { SupabaseIcon } from '../supabase-icon'
7 |
8 | export type IntegrationDialogProps = DialogProps & {
9 | onConfirm?: () => void
10 | }
11 |
12 | export function IntegrationDialog({ onConfirm, ...props }: IntegrationDialogProps) {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | Connect Supabase
20 |
21 |
22 |
23 |
24 |
25 | To deploy your database, you need to connect your Supabase account. If you don't
26 | already have a Supabase account, you can create one for free.
27 |
28 |
29 | Click Connect to connect your account.
30 |
31 |
Connect
32 |
33 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/apps/web/components/model-provider/use-model-provider.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useCallback } from 'react'
2 | import * as kv from 'idb-keyval'
3 |
4 | export type ModelProvider = {
5 | apiKey?: string
6 | model: string
7 | baseUrl: string
8 | system: string
9 | enabled: boolean
10 | }
11 |
12 | let configStore: kv.UseStore
13 |
14 | export function getConfigStore() {
15 | if (configStore) {
16 | return configStore
17 | }
18 | configStore = kv.createStore('/database.build/config', 'config')
19 | return configStore
20 | }
21 |
22 | export function useModelProvider() {
23 | const [modelProvider, setModelProvider] = useState()
24 |
25 | const set = useCallback(async (modelProvider: ModelProvider) => {
26 | await kv.set('modelProvider', modelProvider, getConfigStore())
27 | setModelProvider(modelProvider)
28 | }, [])
29 |
30 | const remove = useCallback(async () => {
31 | await kv.del('modelProvider', getConfigStore())
32 | setModelProvider(undefined)
33 | }, [])
34 |
35 | useEffect(() => {
36 | async function init() {
37 | const modelProvider = await kv.get('modelProvider', getConfigStore())
38 | setModelProvider(modelProvider)
39 | }
40 | init()
41 | }, [setModelProvider])
42 |
43 | return {
44 | state: modelProvider,
45 | set,
46 | delete: remove,
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/apps/browser-proxy/src/pg-dump-middleware/utils.ts:
--------------------------------------------------------------------------------
1 | export function parseRowDescription(message: Uint8Array): string[] {
2 | const fieldCount = new DataView(message.buffer, message.byteOffset + 5, 2).getUint16(0)
3 | const names: string[] = []
4 | let offset = 7
5 |
6 | for (let i = 0; i < fieldCount; i++) {
7 | const nameEnd = message.indexOf(0, offset)
8 | names.push(new TextDecoder().decode(message.subarray(offset, nameEnd)))
9 | offset = nameEnd + 19 // Skip null terminator and 18 bytes of field info
10 | }
11 |
12 | return names
13 | }
14 |
15 | export function parseDataRowFields(
16 | message: Uint8Array
17 | ): { value: string | null; length: number }[] {
18 | const fieldCount = new DataView(message.buffer, message.byteOffset + 5, 2).getUint16(0)
19 | const fields: { value: string | null; length: number }[] = []
20 | let offset = 7
21 |
22 | for (let i = 0; i < fieldCount; i++) {
23 | const fieldLength = new DataView(message.buffer, message.byteOffset + offset, 4).getInt32(0)
24 | offset += 4
25 |
26 | if (fieldLength === -1) {
27 | fields.push({ value: null, length: -1 })
28 | } else {
29 | fields.push({
30 | value: new TextDecoder().decode(message.subarray(offset, offset + fieldLength)),
31 | length: fieldLength,
32 | })
33 | offset += fieldLength
34 | }
35 | }
36 |
37 | return fields
38 | }
39 |
--------------------------------------------------------------------------------
/apps/web/components/layout/header/deploy-button/connect-supabase-tab.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-unescaped-entities */
2 | import { useRouter } from 'next/navigation'
3 | import { SupabaseIcon } from '~/components/supabase-icon'
4 | import { Button } from '~/components/ui/button'
5 | import { TabsContent } from '~/components/ui/tabs'
6 | import type { MergedDatabase } from '~/data/merged-databases/merged-database'
7 | import { getOauthUrl } from '~/lib/util'
8 |
9 | export function ConnectSupabaseTab(props: { database: MergedDatabase }) {
10 | const router = useRouter()
11 |
12 | return (
13 |
14 |
15 |
Connect Supabase
16 |
17 | To deploy your database, you need to connect your Supabase account. If you don't already
18 | have a Supabase account, you can create one for free.
19 |
20 |
{
25 | router.push(getOauthUrl({ databaseId: props.database.id }))
26 | }}
27 | >
28 | Connect
29 |
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/apps/browser-proxy/src/tls.ts:
--------------------------------------------------------------------------------
1 | import { Buffer } from 'node:buffer'
2 | import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'
3 | import pMemoize from 'p-memoize'
4 | import ExpiryMap from 'expiry-map'
5 | import type { Server } from 'node:https'
6 |
7 | const s3Client = new S3Client({ forcePathStyle: true })
8 |
9 | async function _getTls() {
10 | const cert = await s3Client
11 | .send(
12 | new GetObjectCommand({
13 | Bucket: process.env.AWS_S3_BUCKET,
14 | Key: `tls/${process.env.WILDCARD_DOMAIN}/cert.pem`,
15 | })
16 | )
17 | .then(({ Body }) => Body?.transformToByteArray())
18 |
19 | const key = await s3Client
20 | .send(
21 | new GetObjectCommand({
22 | Bucket: process.env.AWS_S3_BUCKET,
23 | Key: `tls/${process.env.WILDCARD_DOMAIN}/key.pem`,
24 | })
25 | )
26 | .then(({ Body }) => Body?.transformToByteArray())
27 |
28 | if (!cert || !key) {
29 | throw new Error('TLS certificate or key not found')
30 | }
31 |
32 | return {
33 | cert: Buffer.from(cert),
34 | key: Buffer.from(key),
35 | }
36 | }
37 |
38 | // cache the TLS certificate for 1 week
39 | const cache = new ExpiryMap(1000 * 60 * 60 * 24 * 7)
40 | export const getTls = pMemoize(_getTls, { cache })
41 |
42 | export async function setSecureContext(httpsServer: Server) {
43 | const tlsOptions = await getTls()
44 | httpsServer.setSecureContext(tlsOptions)
45 | }
46 |
--------------------------------------------------------------------------------
/apps/web/data/databases/database-update-mutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
2 | import { useApp } from '~/components/app-provider'
3 | import { Database } from '~/lib/db'
4 | import { getDatabaseQueryKey } from './database-query'
5 | import { getDatabasesQueryKey } from './databases-query'
6 |
7 | export type DatabaseUpdateVariables = {
8 | id: string
9 | name: string | null
10 | isHidden: boolean
11 | }
12 |
13 | export const useDatabaseUpdateMutation = ({
14 | onSuccess,
15 | onError,
16 | ...options
17 | }: Omit, 'mutationFn'> = {}) => {
18 | const { dbManager } = useApp()
19 | const queryClient = useQueryClient()
20 |
21 | return useMutation({
22 | mutationFn: async ({ id, name, isHidden }) => {
23 | if (!dbManager) {
24 | throw new Error('dbManager is not available')
25 | }
26 | return await dbManager.updateDatabase(id, { name, isHidden })
27 | },
28 | async onSuccess(data, variables, context) {
29 | await Promise.all([queryClient.invalidateQueries({ queryKey: getDatabasesQueryKey() })])
30 | await Promise.all([
31 | queryClient.invalidateQueries({ queryKey: getDatabaseQueryKey(variables.id) }),
32 | ])
33 | return onSuccess?.(data, variables, context)
34 | },
35 | ...options,
36 | })
37 | }
38 |
--------------------------------------------------------------------------------
/apps/web/components/layout/header/live-share-button.tsx:
--------------------------------------------------------------------------------
1 | import { useApp } from '~/components/app-provider'
2 | import { LiveShareIcon } from '~/components/live-share-icon'
3 | import { Button } from '~/components/ui/button'
4 | import { Tooltip, TooltipContent, TooltipTrigger } from '~/components/ui/tooltip'
5 | import type { MergedDatabase } from '~/data/merged-databases/merged-database'
6 | import { cn } from '~/lib/utils'
7 |
8 | export function LiveShareButton(props: { database: MergedDatabase }) {
9 | const { liveShare, user } = useApp()
10 |
11 | const handleClick = () => {
12 | if (!user) return
13 | liveShare.start(props.database.id)
14 | }
15 |
16 | return (
17 |
18 |
19 |
25 | {liveShare.isLiveSharing && (
26 |
27 | )}
28 |
29 |
30 |
31 |
32 | {!user
33 | ? 'Sign in to use Live Share'
34 | : liveShare.isLiveSharing
35 | ? 'Stop Live Share'
36 | : 'Start Live Share'}
37 |
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/apps/web/components/tools/conversation-rename.tsx:
--------------------------------------------------------------------------------
1 | import { ToolInvocation } from '~/lib/tools'
2 | import { useApp } from '../app-provider'
3 | import { useWorkspace } from '../workspace'
4 |
5 | export type CsvRequestProps = {
6 | toolInvocation: ToolInvocation<'renameConversation'>
7 | }
8 |
9 | export default function ConversationRename({ toolInvocation }: CsvRequestProps) {
10 | const { user } = useApp()
11 | const { appendMessage } = useWorkspace()
12 |
13 | if (!('result' in toolInvocation)) {
14 | return null
15 | }
16 |
17 | const { args, result } = toolInvocation
18 |
19 | if (!result.success) {
20 | // TODO: show error to the user
21 | return (
22 | Error renaming conversation
23 | )
24 | }
25 |
26 | const { name } = args
27 |
28 | return (
29 |
30 |
31 | Conversation renamed to {name} {' '}
32 | {user && (
33 | {
36 | appendMessage({
37 | role: 'user',
38 | content: "Let's rename the conversation. Any suggestions?",
39 | })
40 | }}
41 | >
42 | change
43 |
44 | )}
45 |
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/apps/web/components/layout/header/header.tsx:
--------------------------------------------------------------------------------
1 | import { Breadcrumbs } from './breadcrumb'
2 | import { useParams } from 'next/navigation'
3 | import { MenuButton } from './menu-button'
4 | import { CreateDatabaseButton } from './create-database-button'
5 | import { DeployButton } from './deploy-button/deploy-button'
6 | import { LiveShareButton } from './live-share-button'
7 | import { ExtraDatabaseActionsButton } from './extra-database-actions-button'
8 | import ByoLlmButton from '~/components/byo-llm-button'
9 | import { UserAvatar } from './user'
10 | import { useMergedDatabase } from '~/data/merged-databases/merged-database'
11 |
12 | export function Header() {
13 | const { id } = useParams<{ id: string }>()
14 | const { data: database } = useMergedDatabase(id)
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {database ? (
28 | <>
29 |
30 |
31 |
32 | >
33 | ) : null}
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/apps/web/components/schema/legend.tsx:
--------------------------------------------------------------------------------
1 | import { DiamondIcon, Fingerprint, Hash, Key } from 'lucide-react'
2 |
3 | const SchemaGraphLegend = () => {
4 | return (
5 |
6 |
7 |
8 |
9 | Primary key
10 |
11 |
12 |
13 | Identity
14 |
15 |
16 |
17 | Unique
18 |
19 |
20 |
21 | Nullable
22 |
23 |
24 |
30 | Non-Nullable
31 |
32 |
33 |
34 | )
35 | }
36 |
37 | export default SchemaGraphLegend
38 |
--------------------------------------------------------------------------------
/apps/web/data/integrations/integration-query.ts:
--------------------------------------------------------------------------------
1 | import { UseQueryOptions, useQuery } from '@tanstack/react-query'
2 | import { IntegrationDetails } from '~/app/api/integrations/[id]/details/route'
3 | import { createClient } from '~/utils/supabase/client'
4 |
5 | async function getIntegrationDetails(id: number): Promise {
6 | const response = await fetch(`/api/integrations/${id}/details`)
7 |
8 | if (!response.ok) {
9 | throw new Error('Failed to fetch integration details')
10 | }
11 |
12 | return await response.json()
13 | }
14 |
15 | async function getIntegration(name: string) {
16 | const supabase = createClient()
17 |
18 | const { data, error } = await supabase
19 | .from('deployment_provider_integrations')
20 | .select('id, deployment_providers!inner()')
21 | .eq('deployment_providers.name', name)
22 | .is('revoked_at', null)
23 | .single()
24 |
25 | if (error) {
26 | throw error
27 | }
28 |
29 | return data
30 | }
31 |
32 | export const useIntegrationQuery = (
33 | name: string,
34 | options: Omit, 'queryKey' | 'queryFn'> = {}
35 | ) => {
36 | return useQuery({
37 | ...options,
38 | queryKey: getIntegrationQueryKey(name),
39 | queryFn: async () => {
40 | const { id } = await getIntegration(name)
41 | return await getIntegrationDetails(id)
42 | },
43 | retry: false,
44 | })
45 | }
46 |
47 | export const getIntegrationQueryKey = (name: string) => ['integration', name]
48 |
--------------------------------------------------------------------------------
/apps/web/components/layout/header/toggle-theme-button.tsx:
--------------------------------------------------------------------------------
1 | import { MoonIcon, SunIcon } from 'lucide-react'
2 | import { useTheme } from 'next-themes'
3 | import { Button } from '~/components/ui/button'
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuItem,
8 | DropdownMenuTrigger,
9 | } from '~/components/ui/dropdown-menu'
10 | import { Tooltip, TooltipContent, TooltipTrigger } from '~/components/ui/tooltip'
11 |
12 | export function ThemeToggleButton() {
13 | const { setTheme } = useTheme()
14 |
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 | Toggle theme
30 |
31 |
32 | setTheme('light')}>Light
33 | setTheme('dark')}>Dark
34 | setTheme('system')}>System
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/apps/web/components/tools/generated-chart.tsx:
--------------------------------------------------------------------------------
1 | import { m } from 'framer-motion'
2 | import { Chart } from 'react-chartjs-2'
3 | import { ErrorBoundary } from 'react-error-boundary'
4 | import { ToolInvocation } from '~/lib/tools'
5 |
6 | export type GeneratedChartProps = {
7 | toolInvocation: ToolInvocation<'generateChart'>
8 | }
9 |
10 | export default function GeneratedChart({ toolInvocation }: GeneratedChartProps) {
11 | if (!('result' in toolInvocation)) {
12 | return null
13 | }
14 |
15 | if ('error' in toolInvocation.result) {
16 | return Error loading chart
17 | }
18 |
19 | const { type, data, options } = toolInvocation.args.config
20 | return (
21 | (
23 | Error loading chart
24 | )}
25 | >
26 |
39 |
48 |
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/apps/web/components/tools/index.tsx:
--------------------------------------------------------------------------------
1 | import { ToolInvocation } from '~/lib/tools'
2 | import ConversationRename from './conversation-rename'
3 | import CsvExport from './csv-export'
4 | import CsvImport from './csv-import'
5 | import CsvRequest from './csv-request'
6 | import ExecutedSql from './executed-sql'
7 | import GeneratedChart from './generated-chart'
8 | import GeneratedEmbedding from './generated-embedding'
9 | import SqlImport from './sql-import'
10 | import SqlRequest from './sql-request'
11 |
12 | export type ToolUiProps = {
13 | toolInvocation: ToolInvocation
14 | }
15 |
16 | export function ToolUi({ toolInvocation }: ToolUiProps) {
17 | switch (toolInvocation.toolName) {
18 | case 'executeSql':
19 | return
20 | case 'generateChart':
21 | return
22 | case 'requestCsv':
23 | return
24 | case 'importCsv':
25 | return
26 | case 'exportCsv':
27 | return
28 | case 'requestSql':
29 | return
30 | case 'importSql':
31 | return
32 | case 'renameConversation':
33 | return
34 | case 'embed':
35 | return
36 | }
37 | return null
38 | }
39 |
--------------------------------------------------------------------------------
/apps/web/components/providers.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
4 |
5 | import { PropsWithChildren, useEffect } from 'react'
6 | import AppProvider from './app-provider'
7 | import { LockProvider } from './lock-provider'
8 | import { ThemeProvider } from './theme-provider'
9 |
10 | const queryClient = new QueryClient()
11 |
12 | async function registerServiceWorker() {
13 | try {
14 | const reg = await navigator.serviceWorker.getRegistration()
15 |
16 | // If this was a hard refresh (no controller), browsers will disable service workers
17 | // We should soft reload the page to ensure the service worker is active
18 | if (reg?.active && !navigator.serviceWorker.controller) {
19 | window.location.reload()
20 | }
21 | await navigator.serviceWorker.register('/sw.mjs', { scope: '/' })
22 | } catch (error) {
23 | console.error('Failed to register service worker', error)
24 | }
25 | }
26 |
27 | export default function Providers({ children }: PropsWithChildren) {
28 | useEffect(() => {
29 | if ('serviceWorker' in navigator) {
30 | registerServiceWorker()
31 | }
32 | }, [])
33 |
34 | return (
35 |
36 |
37 |
38 | {children}
39 |
40 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/apps/web/components/byo-llm-button.tsx:
--------------------------------------------------------------------------------
1 | import { Brain } from 'lucide-react'
2 | import { useApp } from '~/components/app-provider'
3 | import { Button } from '~/components/ui/button'
4 | import { cn } from '~/lib/utils'
5 | import { Tooltip, TooltipContent, TooltipTrigger } from '~/components/ui/tooltip'
6 |
7 | export type ByoLlmButtonProps = {
8 | onClick?: () => void
9 | className?: string
10 | size?: 'default' | 'sm' | 'lg' | 'icon'
11 | iconOnly?: boolean
12 | }
13 |
14 | export default function ByoLlmButton({
15 | onClick,
16 | className,
17 | size = 'default',
18 | iconOnly = false,
19 | }: ByoLlmButtonProps) {
20 | const { setIsModelProviderDialogOpen, modelProvider } = useApp()
21 |
22 | const button = (
23 | {
28 | onClick?.()
29 | setIsModelProviderDialogOpen(true)
30 | }}
31 | >
32 | {modelProvider?.state?.enabled && (
33 |
34 | )}
35 |
36 | {!iconOnly && 'Bring your own LLM'}
37 |
38 | )
39 |
40 | if (iconOnly) {
41 | return (
42 |
43 | {button}
44 | Bring your own LLM
45 |
46 | )
47 | }
48 |
49 | return button
50 | }
51 |
--------------------------------------------------------------------------------
/apps/web/components/theme-dropdown.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Moon, Sun } from 'lucide-react'
4 | import { Button } from './ui/button'
5 | import {
6 | DropdownMenu,
7 | DropdownMenuContent,
8 | DropdownMenuItem,
9 | DropdownMenuTrigger,
10 | } from './ui/dropdown-menu'
11 | import { useTheme } from 'next-themes'
12 | import { cn } from '~/lib/utils'
13 |
14 | export type ThemeDropdownProps = {
15 | iconOnly?: boolean
16 | className?: string
17 | }
18 |
19 | export default function ThemeDropdown({ iconOnly = false, className }: ThemeDropdownProps) {
20 | const { setTheme } = useTheme()
21 |
22 | return (
23 |
24 |
25 |
30 |
31 |
32 | {!iconOnly && Toggle theme }
33 |
34 |
35 |
36 | setTheme('light')}>Light
37 | setTheme('dark')}>Dark
38 | setTheme('system')}>System
39 |
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/apps/browser-proxy/src/telemetry.ts:
--------------------------------------------------------------------------------
1 | class BaseEvent {
2 | event_message: string
3 | metadata: Record
4 | constructor(event_message: string, metadata: Record) {
5 | this.event_message = event_message
6 | this.metadata = metadata
7 | }
8 | }
9 |
10 | export class DatabaseShared extends BaseEvent {
11 | constructor(metadata: { databaseId: string; userId: string }) {
12 | super('database-shared', metadata)
13 | }
14 | }
15 |
16 | export class DatabaseUnshared extends BaseEvent {
17 | constructor(metadata: { databaseId: string; userId: string }) {
18 | super('database-unshared', metadata)
19 | }
20 | }
21 |
22 | export class UserConnected extends BaseEvent {
23 | constructor(metadata: { databaseId: string; connectionId: string }) {
24 | super('user-connected', metadata)
25 | }
26 | }
27 |
28 | export class UserDisconnected extends BaseEvent {
29 | constructor(metadata: { databaseId: string; connectionId: string }) {
30 | super('user-disconnected', metadata)
31 | }
32 | }
33 |
34 | type Event = DatabaseShared | DatabaseUnshared | UserConnected | UserDisconnected
35 |
36 | export async function logEvent(event: Event) {
37 | if (process.env.LOGFLARE_SOURCE_URL) {
38 | fetch(process.env.LOGFLARE_SOURCE_URL, {
39 | method: 'POST',
40 | headers: {
41 | 'Content-Type': 'application/json',
42 | },
43 | body: JSON.stringify(event),
44 | }).catch((err) => {
45 | console.error(err)
46 | })
47 | } else if (process.env.DEBUG) {
48 | console.log(event)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/apps/web/components/sidebar/database-menu-item.tsx:
--------------------------------------------------------------------------------
1 | import { TooltipPortal } from '@radix-ui/react-tooltip'
2 | import { RadioIcon } from 'lucide-react'
3 | import Link from 'next/link'
4 | import { useApp } from '~/components/app-provider'
5 | import { Tooltip, TooltipContent, TooltipTrigger } from '~/components/ui/tooltip'
6 | import type { MergedDatabase } from '~/data/merged-databases/merged-database'
7 | import { cn } from '~/lib/utils'
8 |
9 | export type DatabaseMenuItemProps = {
10 | database: MergedDatabase
11 | isActive: boolean
12 | onClick?: () => void
13 | }
14 |
15 | export function DatabaseMenuItem({ database, isActive, onClick }: DatabaseMenuItemProps) {
16 | const { liveShare } = useApp()
17 |
18 | return (
19 |
27 | {liveShare.isLiveSharing && liveShare.databaseId === database.id && (
28 |
29 |
30 |
31 |
32 |
33 |
34 | Shared
35 |
36 |
37 |
38 | )}
39 | {database.name ?? 'My database'}
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/apps/web/components/copyable-field.tsx:
--------------------------------------------------------------------------------
1 | import { CopyIcon } from 'lucide-react'
2 | import { type ReactNode, useState } from 'react'
3 | import { Button } from '~/components/ui/button'
4 | import { Input } from '~/components/ui/input'
5 | import { Label } from '~/components/ui/label'
6 |
7 | export function CopyableField(props: { label?: ReactNode; value: string; disableCopy?: boolean }) {
8 | return (
9 |
10 | {props.label && {props.label} }
11 |
12 |
13 | )
14 | }
15 |
16 | export function CopyableInput(props: { value: string; disableCopy?: boolean }) {
17 | const [isCopying, setIsCopying] = useState(false)
18 |
19 | function handleCopy(value: string) {
20 | setIsCopying(true)
21 | navigator.clipboard.writeText(value)
22 | setTimeout(() => {
23 | setIsCopying(false)
24 | }, 2000)
25 | }
26 |
27 | return (
28 |
29 |
30 | {!props.disableCopy && (
31 | handleCopy(props.value)}
35 | >
36 |
37 | {isCopying ? 'Copied' : 'Copy'}
38 |
39 | )}
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/apps/web/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "~/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/apps/web/next.config.mjs:
--------------------------------------------------------------------------------
1 | import { createRequire } from 'module'
2 | import { readFile } from 'node:fs/promises'
3 | import { join } from 'node:path'
4 | import webpack from 'webpack'
5 |
6 | /** @type {import('next').NextConfig} */
7 | const nextConfig = {
8 | reactStrictMode: false,
9 | env: {
10 | NEXT_PUBLIC_PGLITE_VERSION: await getPackageVersion('@electric-sql/pglite'),
11 | },
12 | webpack: (config) => {
13 | config.resolve = {
14 | ...config.resolve,
15 | fallback: {
16 | fs: false,
17 | module: false,
18 | 'stream/promises': false,
19 | },
20 | }
21 |
22 | // Polyfill `ReadableStream`
23 | config.plugins.push(
24 | new webpack.ProvidePlugin({
25 | ReadableStream: [join(import.meta.dirname, 'polyfills/readable-stream.ts'), 'default'],
26 | })
27 | )
28 |
29 | // See https://webpack.js.org/configuration/resolve/#resolvealias
30 | config.resolve.alias = {
31 | ...config.resolve.alias,
32 | sharp$: false,
33 | 'onnxruntime-node$': false,
34 | }
35 | return config
36 | },
37 | swcMinify: false,
38 | }
39 |
40 | export default nextConfig
41 |
42 | async function getPackageJson(module) {
43 | const require = createRequire(import.meta.url)
44 | const entryPoint = require.resolve(module)
45 | const [nodeModulePath] = entryPoint.split(module)
46 |
47 | const packagePath = join(nodeModulePath, module, 'package.json')
48 | const packageJson = JSON.parse(await readFile(packagePath, 'utf8'))
49 |
50 | return packageJson
51 | }
52 |
53 | async function getPackageVersion(module) {
54 | const packageJson = await getPackageJson(module)
55 | return packageJson.version
56 | }
57 |
--------------------------------------------------------------------------------
/apps/web/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as PopoverPrimitive from '@radix-ui/react-popover'
5 |
6 | import { cn } from '~/lib/utils'
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | const PopoverSeparator = React.forwardRef>(
32 | ({ className, children, ...props }, ref) => (
33 |
34 | )
35 | )
36 | PopoverSeparator.displayName = 'PopoverSeparator'
37 |
38 | export { Popover, PopoverTrigger, PopoverContent, PopoverSeparator }
39 |
--------------------------------------------------------------------------------
/apps/web/components/sidebar/sign-in-dialog.tsx:
--------------------------------------------------------------------------------
1 | import ByoLlmButton from '~/components/byo-llm-button'
2 | import SignInButton from '~/components/sign-in-button'
3 | import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui/dialog'
4 |
5 | export type SignInDialogProps = {
6 | open: boolean
7 | onOpenChange: (open: boolean) => void
8 | }
9 |
10 | export function SignInDialog({ open, onOpenChange }: SignInDialogProps) {
11 | return (
12 |
13 |
14 |
15 | Sign in to create a database
16 |
17 |
18 | Why do I need to sign in?
19 |
20 | Even though your Postgres databases run{' '}
21 |
27 | directly in the browser
28 |
29 | , we still need to connect to an API that runs the large language model (required for all
30 | database interactions).
31 |
32 | We ask you to sign in to prevent API abuse.
33 |
34 |
35 |
36 | or
37 | {
39 | onOpenChange(false)
40 | }}
41 | />
42 |
43 |
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/apps/browser-proxy/src/create-message.ts:
--------------------------------------------------------------------------------
1 | export function createStartupMessage(
2 | user: string,
3 | database: string,
4 | additionalParams: Record = {}
5 | ): Uint8Array {
6 | const encoder = new TextEncoder()
7 |
8 | // Protocol version number (3.0)
9 | const protocolVersion = 196608
10 |
11 | // Combine required and additional parameters
12 | const params = {
13 | user,
14 | database,
15 | ...additionalParams,
16 | }
17 |
18 | // Calculate total message length
19 | let messageLength = 4 // Protocol version
20 | for (const [key, value] of Object.entries(params)) {
21 | messageLength += key.length + 1 + value.length + 1
22 | }
23 | messageLength += 1 // Null terminator
24 |
25 | const uint8Array = new Uint8Array(4 + messageLength)
26 | const view = new DataView(uint8Array.buffer)
27 |
28 | let offset = 0
29 | view.setInt32(offset, messageLength + 4, false) // Total message length (including itself)
30 | offset += 4
31 | view.setInt32(offset, protocolVersion, false) // Protocol version number
32 | offset += 4
33 |
34 | // Write key-value pairs
35 | for (const [key, value] of Object.entries(params)) {
36 | uint8Array.set(encoder.encode(key), offset)
37 | offset += key.length
38 | uint8Array.set([0], offset++) // Null terminator for key
39 | uint8Array.set(encoder.encode(value), offset)
40 | offset += value.length
41 | uint8Array.set([0], offset++) // Null terminator for value
42 | }
43 |
44 | uint8Array.set([0], offset) // Final null terminator
45 |
46 | return uint8Array
47 | }
48 |
49 | export function createTerminateMessage(): Uint8Array {
50 | const uint8Array = new Uint8Array(5)
51 | const view = new DataView(uint8Array.buffer)
52 | view.setUint8(0, 'X'.charCodeAt(0))
53 | view.setUint32(1, 4, false)
54 | return uint8Array
55 | }
56 |
--------------------------------------------------------------------------------
/packages/deploy/src/supabase/types.ts:
--------------------------------------------------------------------------------
1 | import { SupabaseClient as SupabaseClientGeneric } from '@supabase/supabase-js'
2 | import type { Database as SupabaseDatabase } from './database-types.js'
3 | import type { createManagementApiClient } from './management-api/client.js'
4 | import type { paths } from './management-api/types.js'
5 |
6 | export type Credentials = { expiresAt: string; refreshToken: string; accessToken: string }
7 |
8 | export type Project =
9 | paths['/v1/projects/{ref}']['get']['responses']['200']['content']['application/json']
10 |
11 | type Unpacked = T extends (infer U)[] ? U : T
12 |
13 | type Database = Unpacked<
14 | paths['/v1/projects/{ref}/config/database/pooler']['get']['responses']['200']['content']['application/json']
15 | >
16 |
17 | export type Region =
18 | paths['/v1/projects']['post']['requestBody']['content']['application/json']['region']
19 |
20 | export type SupabaseProviderMetadata = {
21 | project: {
22 | id: Project['id']
23 | organizationId: Project['organization_id']
24 | name: Project['name']
25 | region: Project['region']
26 | createdAt: Project['created_at']
27 | database: {
28 | host: NonNullable['host']
29 | name: string
30 | port: number
31 | user: string
32 | }
33 | pooler: {
34 | host: Database['db_host']
35 | name: Database['db_name']
36 | port: number
37 | user: Database['db_user']
38 | }
39 | }
40 | }
41 |
42 | export type SupabaseClient = SupabaseClientGeneric
43 |
44 | export type ManagementApiClient = Awaited>
45 |
46 | export type SupabasePlatformConfig = {
47 | url: string
48 | apiUrl: string
49 | oauthClientId: string
50 | oauthSecret: string
51 | }
52 |
53 | export type SupabaseDeploymentConfig = {
54 | region: Region
55 | }
56 |
--------------------------------------------------------------------------------
/supabase/functions/certificate/README.md:
--------------------------------------------------------------------------------
1 | # Certificate function
2 |
3 | This function manages the SSL certificates for the various services. The certificate is delivered by Let's Encrypt and stored in Supabase Storage under the `tls/` directory.
4 |
5 | When the certificate is about to expire (less than 30 days), the function will renew it.
6 |
7 | ## Setup
8 |
9 | This function requires all these extensions to be enabled:
10 |
11 | - `pg_cron`
12 | - `pg_net`
13 | - `vault`
14 |
15 | The cron job relies on two secrets being present in Supabase Vault:
16 |
17 | ```sql
18 | select vault.create_secret('', 'supabase_url', 'Supabase API URL');
19 | select vault.create_secret(encode(gen_random_bytes(24), 'base64'), 'supabase_functions_certificate_secret', 'Shared secret to trigger the "certificate" Supabase Edge Function');
20 | ```
21 |
22 | Now you can schedule a new weekly cron job with `pg_cron`:
23 |
24 | ```sql
25 | select cron.schedule (
26 | 'certificates',
27 | -- every Sunday at 00:00
28 | '0 0 * * 0',
29 | $$
30 | -- certificate for the browser proxy
31 | select net.http_post(
32 | url:=(select supabase_url() || '/functions/v1/certificate'),
33 | headers:=('{"Content-Type": "application/json", "Authorization": "Bearer ' || (select supabase_functions_certificate_secret()) || '"}')::jsonb,
34 | body:='{"domainName": "browser.db.build"}'::jsonb
35 | ) as request_id
36 | $$
37 | );
38 | ```
39 |
40 | If you immediately want a certificate, you can call the Edge Function manually:
41 |
42 | ```sql
43 | select net.http_post(
44 | url:=(select supabase_url() || '/functions/v1/certificate'),
45 | headers:=('{"Content-Type": "application/json", "Authorization": "Bearer ' || (select supabase_functions_certificate_secret()) || '"}')::jsonb,
46 | body:='{"domainName": "browser.db.build"}'::jsonb
47 | ) as request_id;
48 | ```
--------------------------------------------------------------------------------
/apps/web/components/supabase-icon.tsx:
--------------------------------------------------------------------------------
1 | export const SupabaseIcon = (props: { size?: number; className?: string }) => {
2 | const aspectRatio = 113 / 109
3 | const width = props.size ?? 16
4 | const height = Math.round(width * aspectRatio)
5 |
6 | return (
7 |
14 |
18 |
23 |
27 |
28 |
36 |
37 |
38 |
39 |
47 |
48 |
49 |
50 |
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/apps/web/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '~/lib/utils'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13 | destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
14 | outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
15 | secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
16 | ghost: 'hover:bg-accent hover:text-accent-foreground',
17 | link: 'text-primary underline-offset-4 hover:underline',
18 | },
19 | size: {
20 | default: 'h-10 px-4 py-2',
21 | sm: 'h-9 rounded-md px-3',
22 | lg: 'h-11 rounded-md px-8',
23 | icon: 'h-10 w-10',
24 | },
25 | },
26 | defaultVariants: {
27 | variant: 'default',
28 | size: 'default',
29 | },
30 | }
31 | )
32 |
33 | export interface ButtonProps
34 | extends React.ButtonHTMLAttributes,
35 | VariantProps {
36 | asChild?: boolean
37 | }
38 |
39 | const Button = React.forwardRef(
40 | ({ className, variant, size, asChild = false, ...props }, ref) => {
41 | const Comp = asChild ? Slot : 'button'
42 | return (
43 |
44 | )
45 | }
46 | )
47 | Button.displayName = 'Button'
48 |
49 | export { Button, buttonVariants }
50 |
--------------------------------------------------------------------------------
/apps/web/components/tools/csv-export.tsx:
--------------------------------------------------------------------------------
1 | import { m } from 'framer-motion'
2 | import { Download } from 'lucide-react'
3 | import { useMemo } from 'react'
4 | import { loadFile } from '~/lib/files'
5 | import { formatSql } from '~/lib/sql-util'
6 | import { ToolInvocation } from '~/lib/tools'
7 | import { downloadFile } from '~/lib/util'
8 | import CodeAccordion from '../code-accordion'
9 |
10 | export type CsvExportProps = {
11 | toolInvocation: ToolInvocation<'exportCsv'>
12 | }
13 |
14 | export default function CsvExport({ toolInvocation }: CsvExportProps) {
15 | const { sql } = toolInvocation.args
16 |
17 | const formattedSql = useMemo(() => formatSql(sql), [sql])
18 |
19 | if (!('result' in toolInvocation)) {
20 | return null
21 | }
22 |
23 | const { result } = toolInvocation
24 |
25 | if (!result.success) {
26 | return (
27 |
33 | )
34 | }
35 |
36 | return (
37 | <>
38 |
39 |
48 |
49 | {
53 | const file = await loadFile(result.fileId)
54 | downloadFile(file)
55 | }}
56 | >
57 | {result.file.name}
58 |
59 |
60 | >
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/apps/web/lib/files.ts:
--------------------------------------------------------------------------------
1 | import { countObjects, getObject, hasObject, listObjects, openDB, putObject } from './indexed-db'
2 |
3 | /**
4 | * Stores a file by ID.
5 | */
6 | export async function saveFile(id: string, file: File) {
7 | const db = await openFileDB()
8 | const transaction = db.transaction('files', 'readwrite')
9 | const store = transaction.objectStore('files')
10 | return await putObject(store, id, file)
11 | }
12 |
13 | /**
14 | * Checks if a file with ID exists.
15 | */
16 | export async function hasFile(id: string) {
17 | const db = await openFileDB()
18 | const transaction = db.transaction('files', 'readonly')
19 | const store = transaction.objectStore('files')
20 | return await hasObject(store, id)
21 | }
22 |
23 | /**
24 | * Retrieves a file by ID.
25 | */
26 | export async function loadFile(id: string) {
27 | const db = await openFileDB()
28 | const transaction = db.transaction('files', 'readonly')
29 | const store = transaction.objectStore('files')
30 | return await getObject(store, id)
31 | }
32 |
33 | /**
34 | * Counts all files.
35 | */
36 | export async function countFiles() {
37 | const db = await openFileDB()
38 | return await countObjects(db, 'files')
39 | }
40 |
41 | /**
42 | * Lists all files via an `AsyncIterable` stream.
43 | */
44 | export async function* listFiles() {
45 | const db = await openFileDB()
46 |
47 | for await (const { key, value } of listObjects(db, 'files')) {
48 | if (typeof key !== 'string') {
49 | throw new Error('Expected file in IndexedDB to have a string key')
50 | }
51 | yield {
52 | id: key,
53 | file: value,
54 | }
55 | }
56 | }
57 |
58 | /**
59 | * Opens the file `IndexedDB` database and creates the
60 | * `file` object store if it doesn't exist.
61 | */
62 | export async function openFileDB() {
63 | return await openDB('/supabase/files', 1, (db) => {
64 | if (!db.objectStoreNames.contains('files')) {
65 | db.createObjectStore('files')
66 | }
67 | })
68 | }
69 |
--------------------------------------------------------------------------------
/apps/browser-proxy/src/connection-manager.ts:
--------------------------------------------------------------------------------
1 | import type { PostgresConnection } from 'pg-gateway'
2 | import type { WebSocket } from 'ws'
3 |
4 | type DatabaseId = string
5 | type ConnectionId = string
6 |
7 | class ConnectionManager {
8 | private socketsByDatabase: Map = new Map()
9 | private sockets: Map = new Map()
10 | private websockets: Map = new Map()
11 |
12 | constructor() {}
13 |
14 | public hasSocketForDatabase(databaseId: DatabaseId) {
15 | return this.socketsByDatabase.has(databaseId)
16 | }
17 |
18 | public getSocket(connectionId: ConnectionId) {
19 | return this.sockets.get(connectionId)
20 | }
21 |
22 | public getSocketForDatabase(databaseId: DatabaseId) {
23 | const connectionId = this.socketsByDatabase.get(databaseId)
24 | return connectionId ? this.sockets.get(connectionId) : undefined
25 | }
26 |
27 | public setSocket(databaseId: DatabaseId, connectionId: ConnectionId, socket: PostgresConnection) {
28 | this.sockets.set(connectionId, socket)
29 | this.socketsByDatabase.set(databaseId, connectionId)
30 | }
31 |
32 | public deleteSocketForDatabase(databaseId: DatabaseId) {
33 | const connectionId = this.socketsByDatabase.get(databaseId)
34 | this.socketsByDatabase.delete(databaseId)
35 | if (connectionId) {
36 | this.sockets.delete(connectionId)
37 | }
38 | }
39 |
40 | public hasWebsocket(databaseId: DatabaseId) {
41 | return this.websockets.has(databaseId)
42 | }
43 |
44 | public getWebsocket(databaseId: DatabaseId) {
45 | return this.websockets.get(databaseId)
46 | }
47 |
48 | public setWebsocket(databaseId: DatabaseId, websocket: WebSocket) {
49 | this.websockets.set(databaseId, websocket)
50 | }
51 |
52 | public deleteWebsocket(databaseId: DatabaseId) {
53 | this.websockets.delete(databaseId)
54 | this.deleteSocketForDatabase(databaseId)
55 | }
56 | }
57 |
58 | export const connectionManager = new ConnectionManager()
59 |
--------------------------------------------------------------------------------
/apps/web/components/deploy/schema-overlap-warning.tsx:
--------------------------------------------------------------------------------
1 | import { SUPABASE_SCHEMAS } from '@database.build/deploy/supabase'
2 | import { Results } from '@electric-sql/pglite'
3 | import { sql } from '@electric-sql/pglite/template'
4 | import { join } from '~/lib/db'
5 | import { useAsyncMemo } from '~/lib/hooks'
6 | import { useApp } from '../app-provider'
7 | import { Loader } from 'lucide-react'
8 |
9 | export type SchemaOverlapWarningProps = {
10 | databaseId: string
11 | }
12 |
13 | export function SchemaOverlapWarning({ databaseId }: SchemaOverlapWarningProps) {
14 | const { dbManager } = useApp()
15 |
16 | const { value: overlappingSchemas, loading: isLoadingSchemas } = useAsyncMemo(async () => {
17 | if (!dbManager) {
18 | throw new Error('dbManager is not available')
19 | }
20 |
21 | const db = await dbManager.getDbInstance(databaseId)
22 |
23 | console.log(SUPABASE_SCHEMAS)
24 |
25 | const { rows }: Results<{ schema_name: string }> = await db.sql`
26 | SELECT schema_name
27 | FROM information_schema.schemata
28 | WHERE schema_name IN (${join(
29 | SUPABASE_SCHEMAS.map((s) => sql`${s}`),
30 | ','
31 | )})
32 | `
33 |
34 | return rows.map((row) => row.schema_name)
35 | }, [databaseId])
36 |
37 | if (isLoadingSchemas || !overlappingSchemas) {
38 | return null
39 | }
40 |
41 | if (overlappingSchemas.length > 0) {
42 | return (
43 |
44 | The following Supabase schemas were detected in your browser database and will be excluded
45 | from the deployment:
46 |
47 |
48 | {overlappingSchemas.map((schema) => (
49 |
50 | {schema}
51 |
52 | ))}
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | return null
60 | }
61 |
--------------------------------------------------------------------------------
/apps/web/data/tables/tables-query.ts:
--------------------------------------------------------------------------------
1 | import {
2 | PostgresMetaBase,
3 | PostgresMetaErr,
4 | PostgresTable,
5 | wrapError,
6 | wrapResult,
7 | } from '@gregnr/postgres-meta/base'
8 | import { UseQueryOptions, useQuery } from '@tanstack/react-query'
9 | import { useApp } from '~/components/app-provider'
10 | import { DbManager } from '~/lib/db'
11 |
12 | export type TablesVariables = {
13 | databaseId: string
14 | schemas?: string[]
15 | }
16 | export type TablesData = PostgresTable[]
17 | export type TablesError = PostgresMetaErr['error']
18 |
19 | export async function getTablesForQuery(
20 | dbManager: DbManager | undefined,
21 | { databaseId, schemas }: TablesVariables
22 | ) {
23 | if (!dbManager) {
24 | throw new Error('dbManager is not available')
25 | }
26 |
27 | const db = await dbManager.getDbInstance(databaseId)
28 |
29 | const pgMeta = new PostgresMetaBase({
30 | query: async (sql) => {
31 | try {
32 | const res = await db.query(sql)
33 | return wrapResult(res.rows)
34 | } catch (error) {
35 | return wrapError(error, sql)
36 | }
37 | },
38 | end: async () => {},
39 | })
40 |
41 | const { data, error } = await pgMeta.tables.list({
42 | includedSchemas: schemas,
43 | includeColumns: true,
44 | })
45 |
46 | if (error) {
47 | throw error
48 | }
49 |
50 | return data
51 | }
52 |
53 | export const useTablesQuery = (
54 | { databaseId, schemas }: TablesVariables,
55 | options: Omit, 'queryKey' | 'queryFn'> = {}
56 | ) => {
57 | const { dbManager } = useApp()
58 | return useQuery({
59 | ...options,
60 | queryKey: getTablesQueryKey({ databaseId, schemas }),
61 | queryFn: () => getTablesForQuery(dbManager, { databaseId, schemas }),
62 | staleTime: Infinity,
63 | })
64 | }
65 |
66 | export const getTablesQueryKey = ({ databaseId, schemas }: TablesVariables) => [
67 | 'tables',
68 | { databaseId, schemas },
69 | ]
70 |
--------------------------------------------------------------------------------
/apps/web/sw.ts:
--------------------------------------------------------------------------------
1 | import { createOpenAI } from '@ai-sdk/openai'
2 | import { convertToCoreMessages, streamText, ToolInvocation } from 'ai'
3 | import * as kv from 'idb-keyval'
4 | import { getConfigStore, type ModelProvider } from '~/components/model-provider/use-model-provider'
5 | import { convertToCoreTools, maxMessageContext, tools } from '~/lib/tools'
6 |
7 | type Message = {
8 | role: 'user' | 'assistant'
9 | content: string
10 | toolInvocations?: (ToolInvocation & { result: any })[]
11 | }
12 |
13 | declare const self: ServiceWorkerGlobalScope
14 |
15 | async function handleRequest(event: FetchEvent) {
16 | const url = new URL(event.request.url)
17 | const isChatRoute = url.pathname.startsWith('/api/chat') && event.request.method === 'POST'
18 | if (isChatRoute) {
19 | const modelProvider = await kv.get('modelProvider', getConfigStore())
20 |
21 | if (!modelProvider?.enabled) {
22 | return fetch(event.request)
23 | }
24 |
25 | const adapter = createOpenAI({
26 | baseURL: modelProvider.baseUrl,
27 | apiKey: modelProvider.apiKey,
28 | })
29 |
30 | const model = adapter(modelProvider.model)
31 |
32 | const { messages }: { messages: Message[] } = await event.request.json()
33 |
34 | // Trim the message context sent to the LLM to mitigate token abuse
35 | const trimmedMessageContext = messages.slice(-maxMessageContext)
36 |
37 | const coreMessages = convertToCoreMessages(trimmedMessageContext)
38 | const coreTools = convertToCoreTools(tools)
39 |
40 | try {
41 | const result = streamText({
42 | system: modelProvider.system,
43 | model,
44 | messages: coreMessages,
45 | tools: coreTools,
46 | })
47 |
48 | return result.toDataStreamResponse()
49 | } catch (error) {
50 | return new Response(`Error streaming LLM from service worker: ${error}`, { status: 500 })
51 | }
52 | }
53 |
54 | return fetch(event.request)
55 | }
56 |
57 | self.addEventListener('fetch', (event) => {
58 | event.respondWith(handleRequest(event))
59 | })
60 |
--------------------------------------------------------------------------------
/apps/web/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as TabsPrimitive from '@radix-ui/react-tabs'
5 |
6 | import { cn } from '~/lib/utils'
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/apps/web/components/sidebar/set-external-model-provider-button.tsx:
--------------------------------------------------------------------------------
1 | import { m } from 'framer-motion'
2 | import { Brain } from 'lucide-react'
3 | import { useApp } from '~/components/app-provider'
4 | import { SetModelProviderDialog } from '~/components/model-provider/set-model-provider-dialog'
5 | import { Button } from '~/components/ui/button'
6 | import { Tooltip, TooltipContent, TooltipTrigger } from '~/components/ui/tooltip'
7 |
8 | type SetExternalModelProviderButtonProps = {
9 | collapsed?: boolean
10 | }
11 |
12 | export function SetExternalModelProviderButton(props: SetExternalModelProviderButtonProps) {
13 | const { modelProvider, isModelProviderDialogOpen, setIsModelProviderDialogOpen } = useApp()
14 |
15 | const modelName = modelProvider.state?.model.split('/').at(-1)
16 | const text = modelProvider.state?.enabled ? modelName : 'Bring your own LLM'
17 | const button = props.collapsed ? (
18 |
19 |
20 |
21 | setIsModelProviderDialogOpen(true)}
25 | >
26 |
27 |
28 |
29 |
30 |
31 | {text}
32 |
33 |
34 | ) : (
35 |
36 | setIsModelProviderDialogOpen(true)}
40 | >
41 |
42 | {text}
43 |
44 |
45 | )
46 |
47 | return (
48 | <>
49 |
53 | {button}
54 | >
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/apps/web/README.md:
--------------------------------------------------------------------------------
1 | # @database.build/web
2 |
3 | In-browser Postgres sandbox with AI assistance. Built on Next.js.
4 |
5 | ## Architecture
6 |
7 | We use PGlite for 2 purposes:
8 |
9 | 1. A "meta" DB that keeps track of all of the user databases along with their message history
10 | 2. A "user" DB for each database the user creates along with whatever tables/data they've created
11 |
12 | Both databases are stored locally in the browser via IndexedDB. This means that these databases are not persisted to the cloud and cannot be accessed from multiple devices (though this is on the roadmap).
13 |
14 | Every PGlite instance runs in a Web Worker so that the main thread is not blocked.
15 |
16 | ## AI
17 |
18 | The AI component is powered by OpenAI's GPT-4o model by default. The project uses [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to simplify message streams and tool calls.
19 |
20 | ### Environment Variables
21 |
22 | In addition to the required `OPENAI_API_KEY`, the following environment variables can be configured:
23 |
24 | - `OPENAI_API_BASE`: (Optional) The base URL for the OpenAI API. Defaults to `https://api.openai.com/v1`.
25 | - `OPENAI_MODEL`: (Optional) The model used by the AI component. Defaults to `gpt-4o-2024-08-06`.
26 |
27 | **NOTE**: The current prompts and tools are designed around the GPT-4o model. If you choose to use a different model, expect different behavior and results. Additionally, ensure that the model you select supports tool (function) call capabilities.
28 |
29 | ## Authentication
30 |
31 | Because LLMs cost money, a lightweight auth wall exists to prevent abuse. It is currently only used to validate that the user has a legitimate GitHub account, but in the future it could be used to save private/public databases to the cloud.
32 |
33 | Authentication and users are managed by a [Supabase](https://supabase.com/) database. You can find the migrations and other configuration for this in the root [`./supabase`](../../supabase/) directory.
34 |
35 | ## Development
36 |
37 | The Next.js server should run from the monorepo root. See [Development](../../README.md#development).
38 |
--------------------------------------------------------------------------------
/apps/web/lib/db/worker.ts:
--------------------------------------------------------------------------------
1 | import { PGlite } from '@electric-sql/pglite'
2 | import { vector } from '@electric-sql/pglite/vector'
3 | import { PGliteWorkerOptions, worker } from '@electric-sql/pglite/worker'
4 | import { pgDump } from '@electric-sql/pglite-tools/pg_dump'
5 | import { codeBlock } from 'common-tags'
6 |
7 | worker({
8 | async init(options: PGliteWorkerOptions) {
9 | const db = new PGlite({
10 | ...options,
11 | extensions: {
12 | ...options.extensions,
13 |
14 | // vector extension needs to be passed directly in the worker vs. main thread
15 | vector,
16 | },
17 | })
18 |
19 | const bc = new BroadcastChannel(`${options.id}:pg-dump`)
20 |
21 | bc.addEventListener('message', async (event) => {
22 | if (event.data.action === 'execute-dump') {
23 | let dump = await pgDump({ pg: db })
24 | // clear prepared statements
25 | await db.query('deallocate all')
26 | let dumpContent = await dump.text()
27 | // patch for old PGlite versions where the vector extension was not included in the dump
28 | if (!dumpContent.includes('CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA public;')) {
29 | const insertPoint = 'ALTER SCHEMA meta OWNER TO postgres;'
30 | const insertPointIndex = dumpContent.indexOf(insertPoint) + insertPoint.length
31 | dumpContent = codeBlock`
32 | ${dumpContent.slice(0, insertPointIndex)}
33 |
34 | --
35 | -- Name: vector; Type: EXTENSION; Schema: -; Owner: -
36 | --
37 |
38 | CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA public;
39 |
40 | ${dumpContent.slice(insertPointIndex)}`
41 |
42 | // Create new blob with modified content
43 | dump = new File([dumpContent], event.data.filename)
44 | }
45 | const url = URL.createObjectURL(dump)
46 | bc.postMessage({
47 | action: 'dump-result',
48 | filename: event.data.filename,
49 | url,
50 | })
51 | }
52 | })
53 |
54 | return db
55 | },
56 | })
57 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/db/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import Link from 'next/link'
4 | import { useRouter } from 'next/navigation'
5 | import { useEffect } from 'react'
6 | import { useApp } from '~/components/app-provider'
7 | import { useAcquireLock, useIsLocked } from '~/components/lock-provider'
8 | import Workspace from '~/components/workspace'
9 | import NewDatabasePage from '../../page'
10 |
11 | export default function Page({ params }: { params: { id: string } }) {
12 | const databaseId = params.id
13 |
14 | const router = useRouter()
15 | const { dbManager } = useApp()
16 | useAcquireLock(databaseId)
17 | const isLocked = useIsLocked(databaseId, true)
18 |
19 | useEffect(() => {
20 | async function run() {
21 | if (!dbManager) {
22 | throw new Error('dbManager is not available')
23 | }
24 |
25 | try {
26 | await dbManager.getDbInstance(databaseId)
27 | } catch (err) {
28 | router.push('/')
29 | }
30 | }
31 | run()
32 | }, [dbManager, databaseId, router])
33 |
34 | if (isLocked) {
35 | return (
36 |
37 |
38 |
39 |
40 | This database is already open in another tab or window.
41 |
42 |
43 | Due to{' '}
44 |
50 | PGlite's single-user mode limitation
51 |
52 | , only one connection is allowed at a time.
53 |
54 |
55 | Please close the database in the other location to access it here.
56 |
57 |
58 |
59 | )
60 | }
61 |
62 | return
63 | }
64 |
--------------------------------------------------------------------------------
/apps/web/utils/telemetry.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Event for an AI chat rate limit. Includes the
3 | * remaining input and output tokens in the rate
4 | * limit window (one of these will be <= 0).
5 | */
6 | export type ChatRateLimitEvent = {
7 | type: 'chat-rate-limit'
8 | metadata: {
9 | databaseId: string
10 | userId: string
11 | inputTokensRemaining: number
12 | outputTokensRemaining: number
13 | }
14 | }
15 |
16 | export type ChatInferenceEventToolResult = {
17 | toolName: string
18 | success: boolean
19 | }
20 |
21 | /**
22 | * Event for an AI chat inference request-response.
23 | * Includes both input and output metadata.
24 | */
25 | export type ChatInferenceEvent = {
26 | type: 'chat-inference'
27 | metadata: {
28 | databaseId: string
29 | userId: string
30 | messageCount: number
31 | inputType: 'user-message' | 'tool-result'
32 | toolResults?: ChatInferenceEventToolResult[]
33 | inputTokens: number
34 | outputTokens: number
35 | finishReason:
36 | | 'stop'
37 | | 'length'
38 | | 'content-filter'
39 | | 'tool-calls'
40 | | 'error'
41 | | 'other'
42 | | 'unknown'
43 | toolCalls?: string[]
44 | }
45 | }
46 |
47 | export type TelemetryEvent = ChatRateLimitEvent | ChatInferenceEvent
48 |
49 | export async function logEvent(type: E['type'], metadata: E['metadata']) {
50 | if (!process.env.LOGFLARE_SOURCE || !process.env.LOGFLARE_API_KEY) {
51 | if (process.env.DEBUG) {
52 | console.log(type, metadata)
53 | }
54 | return
55 | }
56 |
57 | const response = await fetch(
58 | `https://api.logflare.app/logs?source=${process.env.LOGFLARE_SOURCE}`,
59 | {
60 | method: 'POST',
61 | headers: {
62 | 'Content-Type': 'application/json',
63 | 'X-API-KEY': process.env.LOGFLARE_API_KEY,
64 | },
65 | body: JSON.stringify({
66 | event_message: type,
67 | metadata,
68 | }),
69 | }
70 | )
71 |
72 | if (!response.ok) {
73 | const { error } = await response.json()
74 | console.error('failed to send logflare event', error)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/apps/web/components/tools/executed-sql.tsx:
--------------------------------------------------------------------------------
1 | import { Workflow } from 'lucide-react'
2 | import { useMemo } from 'react'
3 | import { useAsyncMemo } from '~/lib/hooks'
4 | import { assertDefined, formatSql, isMigrationStatement } from '~/lib/sql-util'
5 | import { ToolInvocation } from '~/lib/tools'
6 | import CodeAccordion from '../code-accordion'
7 | import { useWorkspace } from '../workspace'
8 |
9 | export type ExecutedSqlProps = {
10 | toolInvocation: ToolInvocation<'executeSql'>
11 | }
12 |
13 | export default function ExecutedSql({ toolInvocation }: ExecutedSqlProps) {
14 | const { sql } = toolInvocation.args
15 |
16 | const { setTab } = useWorkspace()
17 | const formattedSql = useMemo(() => formatSql(sql), [sql])
18 |
19 | const { value: containsMigration } = useAsyncMemo(async () => {
20 | // Dynamically import (browser-only) to prevent SSR errors
21 | const { parseQuery } = await import('libpg-query/wasm')
22 |
23 | const parseResult = await parseQuery(sql)
24 | assertDefined(parseResult.stmts, 'Expected stmts to exist in parse result')
25 |
26 | return parseResult.stmts.some(isMigrationStatement)
27 | }, [sql])
28 |
29 | if (!('result' in toolInvocation)) {
30 | return null
31 | }
32 |
33 | const { result } = toolInvocation
34 |
35 | if (!result.success) {
36 | return (
37 |
43 | )
44 | }
45 |
46 | return (
47 |
48 |
49 | {containsMigration && (
50 |
51 |
{
54 | setTab('diagram')
55 | }}
56 | >
57 |
58 | See diagram
59 |
60 |
61 | )}
62 |
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/apps/web/utils/supabase/middleware.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from '@supabase/ssr'
2 | import { NextResponse, type NextRequest } from 'next/server'
3 |
4 | export async function updateSession(request: NextRequest) {
5 | let supabaseResponse = NextResponse.next({
6 | request,
7 | })
8 |
9 | const supabase = createServerClient(
10 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
11 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
12 | {
13 | cookies: {
14 | getAll() {
15 | return request.cookies.getAll()
16 | },
17 | setAll(cookiesToSet) {
18 | cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
19 | supabaseResponse = NextResponse.next({
20 | request,
21 | })
22 | cookiesToSet.forEach(({ name, value, options }) =>
23 | supabaseResponse.cookies.set(name, value, options)
24 | )
25 | },
26 | },
27 | }
28 | )
29 |
30 | // IMPORTANT: Avoid writing any logic between createServerClient and
31 | // supabase.auth.getUser(). A simple mistake could make it very hard to debug
32 | // issues with users being randomly logged out.
33 |
34 | const {
35 | data: { user },
36 | } = await supabase.auth.getUser()
37 |
38 | // All API routes require authentication
39 | if (!user && request.nextUrl.pathname.startsWith('/api')) {
40 | return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 403 })
41 | }
42 |
43 | // IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
44 | // creating a new response object with NextResponse.next() make sure to:
45 | // 1. Pass the request in it, like so:
46 | // const myNewResponse = NextResponse.next({ request })
47 | // 2. Copy over the cookies, like so:
48 | // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
49 | // 3. Change the myNewResponse object to fit your needs, but avoid changing
50 | // the cookies!
51 | // 4. Finally:
52 | // return myNewResponse
53 | // If this is not done, you may be causing the browser and server to go out
54 | // of sync and terminate the user's session prematurely!
55 |
56 | return supabaseResponse
57 | }
58 |
--------------------------------------------------------------------------------
/apps/web/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as AccordionPrimitive from '@radix-ui/react-accordion'
5 | import { ChevronDown } from 'lucide-react'
6 |
7 | import { cn } from '~/lib/utils'
8 |
9 | const Accordion = AccordionPrimitive.Root
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
16 | ))
17 | AccordionItem.displayName = 'AccordionItem'
18 |
19 | const AccordionTrigger = React.forwardRef<
20 | React.ElementRef,
21 | React.ComponentPropsWithoutRef
22 | >(({ className, children, ...props }, ref) => (
23 |
24 | svg]:rotate-180 outline-none',
28 | className
29 | )}
30 | {...props}
31 | >
32 | {children}
33 |
34 |
35 |
36 | ))
37 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
38 |
39 | const AccordionContent = React.forwardRef<
40 | React.ElementRef,
41 | React.ComponentPropsWithoutRef
42 | >(({ className, children, ...props }, ref) => (
43 |
48 | {children}
49 |
50 | ))
51 |
52 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
53 |
54 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
55 |
--------------------------------------------------------------------------------
/apps/web/polyfills/readable-stream.ts:
--------------------------------------------------------------------------------
1 | // `.from()` is expected by `@std/tar`
2 | ;(globalThis as any).ReadableStream.from ??= function (
3 | iterator: Iterator | AsyncIterator
4 | ) {
5 | return new globalThis.ReadableStream({
6 | async pull(controller) {
7 | try {
8 | const { value, done } = await iterator.next()
9 | if (done) {
10 | controller.close()
11 | } else {
12 | controller.enqueue(value)
13 | }
14 | } catch (err) {
15 | controller.error(err)
16 | }
17 | },
18 | })
19 | }
20 |
21 | // Some browsers don't make `ReadableStream` async iterable (eg. Safari), so polyfill
22 | globalThis.ReadableStream.prototype.values ??= function ({
23 | preventCancel = false,
24 | } = {}): AsyncIterableIterator {
25 | const reader = this.getReader()
26 | return {
27 | async next() {
28 | try {
29 | const { value, done } = await reader.read()
30 | if (done) {
31 | reader.releaseLock()
32 | }
33 | return { value, done }
34 | } catch (e) {
35 | reader.releaseLock()
36 | throw e
37 | }
38 | },
39 | async return(value) {
40 | if (!preventCancel) {
41 | const cancelPromise = reader.cancel(value)
42 | reader.releaseLock()
43 | await cancelPromise
44 | } else {
45 | reader.releaseLock()
46 | }
47 | return { done: true, value }
48 | },
49 | [Symbol.asyncIterator]() {
50 | return this
51 | },
52 | }
53 | }
54 |
55 | globalThis.ReadableStream.prototype[Symbol.asyncIterator] ??=
56 | globalThis.ReadableStream.prototype.values
57 |
58 | // @std/tar conditionally uses `ReadableStreamBYOBReader` which isn't supported in Safari,
59 | // so patch `ReadableStream`'s constructor to prevent using BYOB.
60 | // Webpack's `ProvidePlugin` replaces `ReadableStream` references with this patch
61 | export default class PatchedReadableStream extends globalThis.ReadableStream {
62 | constructor(underlyingSource?: UnderlyingSource, strategy?: QueuingStrategy) {
63 | if (underlyingSource?.type === 'bytes' && !('ReadableStreamBYOBReader' in globalThis)) {
64 | underlyingSource.type = undefined
65 | }
66 | super(underlyingSource, strategy)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/apps/web/components/markdown-accordion.tsx:
--------------------------------------------------------------------------------
1 | import { CircleX, Move3D } from 'lucide-react'
2 | import ReactMarkdown from 'react-markdown'
3 | import remarkGfm from 'remark-gfm'
4 | import {
5 | Accordion,
6 | AccordionContent,
7 | AccordionItem,
8 | AccordionTrigger,
9 | } from '~/components/ui/accordion'
10 | import { cn } from '~/lib/utils'
11 |
12 | export type MarkdownAccordionProps = {
13 | title: string
14 | content: string
15 | error?: string
16 | className?: string
17 | }
18 |
19 | export default function MarkdownAccordion({
20 | title,
21 | content,
22 | error,
23 | className,
24 | }: MarkdownAccordionProps) {
25 | return (
26 |
27 |
35 |
43 |
44 | {error ? (
45 |
49 | ) : (
50 |
54 | )}
55 | {title}
56 |
57 |
58 |
59 |
63 | {content}
64 |
65 | {error && {error}
}
66 |
67 |
68 |
69 | )
70 | }
71 |
--------------------------------------------------------------------------------
/apps/web/lib/system-prompt.ts:
--------------------------------------------------------------------------------
1 | import { codeBlock } from 'common-tags'
2 |
3 | export function getSystemPrompt(options?: { maxRowLimit?: number }) {
4 | const { maxRowLimit = 100 } = options ?? {}
5 |
6 | return codeBlock`
7 | You are a helpful database assistant. Under the hood you have access to an in-browser Postgres database called PGlite (https://github.com/electric-sql/pglite).
8 | Some special notes about this database:
9 | - foreign data wrappers are not supported
10 | - the following extensions are available:
11 | - plpgsql [pre-enabled]
12 | - vector (https://github.com/pgvector/pgvector) [pre-enabled]
13 | - use <=> for cosine distance (default to this)
14 | - use <#> for negative inner product
15 | - use <-> for L2 distance
16 | - use <+> for L1 distance
17 | - note queried vectors will be truncated/redacted due to their size - export as CSV if the full vector is required
18 |
19 | When generating tables, do the following:
20 | - For primary keys, always use "id bigint primary key generated always as identity" (not serial)
21 | - Prefer 'text' over 'varchar'
22 | - Keep explanations brief but helpful
23 | - Don't repeat yourself after creating the table
24 |
25 | When creating sample data:
26 | - Make the data realistic, including joined data
27 | - Check for existing records/conflicts in the table
28 |
29 | When querying data, limit to 5 by default. The maximum number of rows you're allowed to fetch is ${maxRowLimit} (to protect AI from token abuse).
30 | If the user needs to fetch more than ${maxRowLimit} rows at once, they can export the query as a CSV.
31 |
32 | When performing FTS, always use 'simple' (languages aren't available).
33 |
34 | When importing CSVs try to solve the problem yourself (eg. use a generic text column, then refine)
35 | vs. asking the user to change the CSV. No need to select rows after importing.
36 |
37 | You also know math. All math equations and expressions must be written in KaTex and must be wrapped in double dollar \`$$\`:
38 | - Inline: $$\\sqrt{26}$$
39 | - Multiline:
40 | $$
41 | \\sqrt{26}
42 | $$
43 |
44 | No images are allowed. Do not try to generate or link images, including base64 data URLs.
45 |
46 | Feel free to suggest corrections for suspected typos.
47 | `
48 | }
49 |
--------------------------------------------------------------------------------
/apps/web/components/code-accordion.tsx:
--------------------------------------------------------------------------------
1 | import { CircleX, DatabaseZap } from 'lucide-react'
2 | import {
3 | Accordion,
4 | AccordionContent,
5 | AccordionItem,
6 | AccordionTrigger,
7 | } from '~/components/ui/accordion'
8 | import { cn } from '~/lib/utils'
9 | import { CodeBlock } from './code-block'
10 |
11 | export type CodeAccordionProps = {
12 | title: string
13 | language: 'sql'
14 | code: string
15 | error?: string
16 | className?: string
17 | }
18 |
19 | export default function CodeAccordion({
20 | title,
21 | language,
22 | code,
23 | error,
24 | className,
25 | }: CodeAccordionProps) {
26 | return (
27 |
28 |
36 |
44 |
45 | {error ? (
46 |
50 | ) : (
51 |
55 | )}
56 | {title}
57 |
58 |
59 |
60 |
68 | {code}
69 |
70 | {error && {error}
}
71 |
72 |
73 |
74 | )
75 | }
76 |
--------------------------------------------------------------------------------
/apps/web/components/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import 'chart.js/auto'
4 | import 'chartjs-adapter-date-fns'
5 |
6 | import { LazyMotion, m } from 'framer-motion'
7 | import Link from 'next/link'
8 | import { PropsWithChildren } from 'react'
9 | import { SetModelProviderDialog } from '~/components/model-provider/set-model-provider-dialog'
10 | import { TooltipProvider } from '~/components/ui/tooltip'
11 | import { useDatabasesQuery } from '~/data/databases/databases-query'
12 | import { useApp } from './app-provider'
13 | import { Header } from './layout/header/header'
14 | import { SupabaseIcon } from './supabase-icon'
15 |
16 | const loadFramerFeatures = () => import('./framer-features').then((res) => res.default)
17 |
18 | export type LayoutProps = PropsWithChildren
19 |
20 | export default function Layout({ children }: LayoutProps) {
21 | const { user, modelProvider, isModelProviderDialogOpen, setIsModelProviderDialogOpen } = useApp()
22 |
23 | const { data: databases, isLoading: isLoadingDatabases } = useDatabasesQuery()
24 | const isAuthRequired = user === undefined && modelProvider.state?.enabled !== true
25 |
26 | return (
27 |
28 |
29 |
30 | {!isAuthRequired || (!!databases?.length && databases.length > 0) ? (
31 |
32 | ) : (
33 |
34 | database.build
35 |
39 |
40 | a Supabase experiment
41 |
42 |
43 | )}
44 |
45 |
46 | {children}
47 |
48 |
49 |
50 |
54 |
55 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/apps/web/lib/schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | export const tableSchema = z.object({
4 | id: z.number(),
5 | schema: z.string(),
6 | name: z.string(),
7 | rls_enabled: z.boolean(),
8 | rls_forced: z.boolean(),
9 | replica_identity: z.union([
10 | z.literal('DEFAULT'),
11 | z.literal('INDEX'),
12 | z.literal('FULL'),
13 | z.literal('NOTHING'),
14 | ]),
15 | bytes: z.number(),
16 | size: z.string(),
17 | live_rows_estimate: z.number(),
18 | dead_rows_estimate: z.number(),
19 | comment: z.union([z.string(), z.null()]),
20 | columns: z
21 | .array(
22 | z.object({
23 | table_id: z.number(),
24 | schema: z.string(),
25 | table: z.string(),
26 | id: z.string(),
27 | ordinal_position: z.number(),
28 | name: z.string(),
29 | default_value: z.unknown(),
30 | data_type: z.string(),
31 | format: z.string(),
32 | is_identity: z.boolean(),
33 | identity_generation: z.union([z.literal('ALWAYS'), z.literal('BY DEFAULT'), z.null()]),
34 | is_generated: z.boolean(),
35 | is_nullable: z.boolean(),
36 | is_updatable: z.boolean(),
37 | is_unique: z.boolean(),
38 | enums: z.array(z.string()),
39 | check: z.union([z.string(), z.null()]),
40 | comment: z.union([z.string(), z.null()]),
41 | })
42 | )
43 | .optional(),
44 | primary_keys: z.array(
45 | z.object({
46 | schema: z.string(),
47 | table_name: z.string(),
48 | name: z.string(),
49 | table_id: z.number(),
50 | })
51 | ),
52 | relationships: z.array(
53 | z.object({
54 | id: z.number(),
55 | constraint_name: z.string(),
56 | source_schema: z.string(),
57 | source_table_name: z.string(),
58 | source_column_name: z.string(),
59 | target_table_schema: z.string(),
60 | target_table_name: z.string(),
61 | target_column_name: z.string(),
62 | })
63 | ),
64 | })
65 | export type Table = z.infer
66 |
67 | export const resultsSchema = z.object({
68 | rows: z.array(z.record(z.any())),
69 | affectedRows: z.number().optional(),
70 | fields: z.array(
71 | z.object({
72 | name: z.string(),
73 | dataTypeID: z.number(),
74 | })
75 | ),
76 | })
77 | export type Results = z.infer
78 |
79 | export const reportSchema = z.object({ name: z.string(), description: z.string() })
80 | export type Report = z.infer
81 |
82 | export const tabsSchema = z.enum(['chat', 'diagram', 'migrations'])
83 | export type TabValue = z.infer
84 |
--------------------------------------------------------------------------------
/apps/web/components/layout/header/deploy-button/redeploy-supabase-tab.tsx:
--------------------------------------------------------------------------------
1 | import { getDatabaseUrl, getPoolerUrl } from '@database.build/deploy/supabase'
2 | import { useRouter } from 'next/navigation'
3 | import { SupabaseDeployInfo, SupabaseDeploymentInfo } from '~/components/deploy/deploy-info'
4 | import { SupabaseIcon } from '~/components/supabase-icon'
5 | import { Button } from '~/components/ui/button'
6 | import { TabsContent } from '~/components/ui/tabs'
7 | import { DeployedDatabase } from '~/data/deployed-databases/deployed-databases-query'
8 | import { useIntegrationQuery } from '~/data/integrations/integration-query'
9 | import type { MergedDatabase } from '~/data/merged-databases/merged-database'
10 | import { getDeployUrl } from '~/lib/util'
11 |
12 | type SupabaseProject = {
13 | id: string
14 | name: string
15 | pooler: {
16 | host: string
17 | name: string
18 | port: number
19 | user: string
20 | }
21 | region: string
22 | database: {
23 | host: string
24 | name: string
25 | port: number
26 | user: string
27 | }
28 | createdAt: string
29 | organizationId: string
30 | }
31 |
32 | export function RedeploySupabaseTab(props: {
33 | database: MergedDatabase
34 | deployment: DeployedDatabase
35 | }) {
36 | const router = useRouter()
37 | const { data: integration, isLoading: isLoadingIntegration } = useIntegrationQuery('Supabase')
38 |
39 | const { project } = props.deployment.provider_metadata as { project: SupabaseProject }
40 |
41 | const projectUrl = `${process.env.NEXT_PUBLIC_SUPABASE_PLATFORM_URL}/dashboard/project/${project.id}`
42 | const databaseUrl = getDatabaseUrl({ project })
43 | const poolerUrl = getPoolerUrl({ project })
44 |
45 | const deployInfo: SupabaseDeploymentInfo = {
46 | name: project.name,
47 | url: projectUrl,
48 | databaseUrl,
49 | poolerUrl,
50 | createdAt: props.deployment.last_deployment_at
51 | ? new Date(props.deployment.last_deployment_at)
52 | : undefined,
53 | }
54 |
55 | const handleRedeployClick = () => {
56 | const deployUrl = getDeployUrl({
57 | databaseId: props.database.id,
58 | integrationId: integration!.id,
59 | })
60 |
61 | router.push(deployUrl)
62 | }
63 |
64 | return (
65 |
66 | Redeploy to Supabase
67 |
68 |
75 | Redeploy
76 |
77 |
78 | )
79 | }
80 |
--------------------------------------------------------------------------------
/apps/web/components/layout/header/extra-database-actions-button.tsx:
--------------------------------------------------------------------------------
1 | import { DownloadIcon, MoreHorizontalIcon, TrashIcon } from 'lucide-react'
2 | import { useRouter } from 'next/navigation'
3 | import { useApp } from '~/components/app-provider'
4 | import { Button } from '~/components/ui/button'
5 | import {
6 | DropdownMenu,
7 | DropdownMenuContent,
8 | DropdownMenuItem,
9 | DropdownMenuSeparator,
10 | DropdownMenuTrigger,
11 | } from '~/components/ui/dropdown-menu'
12 | import { useDatabaseDeleteMutation } from '~/data/databases/database-delete-mutation'
13 | import { MergedDatabase } from '~/data/merged-databases/merged-database'
14 | import { downloadFileFromUrl, titleToKebabCase } from '~/lib/util'
15 |
16 | export function ExtraDatabaseActionsButton(props: { database: MergedDatabase }) {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | function DownloadMenuItem(props: { database: MergedDatabase }) {
34 | const { dbManager } = useApp()
35 |
36 | const handleDownloadClick = async () => {
37 | if (!dbManager) {
38 | throw new Error('dbManager is not available')
39 | }
40 |
41 | // Ensure the db worker is ready
42 | await dbManager.getDbInstance(props.database.id)
43 |
44 | const bc = new BroadcastChannel(`${props.database.id}:pg-dump`)
45 |
46 | bc.addEventListener('message', (event) => {
47 | if (event.data.action === 'dump-result') {
48 | downloadFileFromUrl(event.data.url, event.data.filename)
49 | bc.close()
50 | }
51 | })
52 |
53 | bc.postMessage({
54 | action: 'execute-dump',
55 | filename: `${titleToKebabCase(props.database.name ?? 'My Database')}-${Date.now()}.sql`,
56 | })
57 | }
58 |
59 | return (
60 |
61 | Download
62 |
63 | )
64 | }
65 |
66 | function DeleteMenuItem(props: { database: MergedDatabase }) {
67 | const router = useRouter()
68 | const { mutateAsync: deleteDatabase } = useDatabaseDeleteMutation()
69 |
70 | const handleDeleteClick = async () => {
71 | await deleteDatabase({ id: props.database.id })
72 | router.push('/')
73 | }
74 |
75 | return (
76 |
77 | Delete
78 |
79 | )
80 | }
81 |
--------------------------------------------------------------------------------
/apps/web/components/layout/header/user.tsx:
--------------------------------------------------------------------------------
1 | import { UserIcon, SunIcon } from 'lucide-react'
2 | import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar'
3 | import { Button } from '~/components/ui/button'
4 | import { useApp } from '~/components/app-provider'
5 | import { DropdownMenu } from '@radix-ui/react-dropdown-menu'
6 | import {
7 | DropdownMenuContent,
8 | DropdownMenuItem,
9 | DropdownMenuTrigger,
10 | DropdownMenuSeparator,
11 | DropdownMenuSub,
12 | DropdownMenuSubContent,
13 | DropdownMenuSubTrigger,
14 | DropdownMenuRadioGroup,
15 | DropdownMenuRadioItem,
16 | } from '~/components/ui/dropdown-menu'
17 | import GitHubIcon from '~/assets/github-icon'
18 | import { useTheme } from 'next-themes'
19 |
20 | export function UserAvatar() {
21 | const { user, signIn } = useApp()
22 | const { theme, setTheme } = useTheme()
23 |
24 | if (!user) {
25 | return (
26 | signIn()}>
27 |
28 | Sign in
29 |
30 | )
31 | }
32 |
33 | const avatarUrl = user.user_metadata.avatar_url ?? null
34 | const username = user.user_metadata.user_name ?? null
35 |
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | Theme
53 |
54 |
55 |
56 | Light
57 | Dark
58 | System
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | )
67 | }
68 |
69 | function SignOutButton() {
70 | const { signOut } = useApp()
71 | return (
72 | {
75 | signOut()
76 | }}
77 | >
78 |
79 | Sign out
80 |
81 | )
82 | }
83 |
--------------------------------------------------------------------------------
/apps/web/components/lines.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { useMemo, useRef, useEffect, useState } from 'react'
4 | import { motion } from 'framer-motion'
5 |
6 | interface LineAnimationProps {
7 | columns?: number
8 | }
9 |
10 | const LineAnimation: React.FC = ({ columns = 100 }) => {
11 | const containerRef = useRef(null)
12 | const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
13 |
14 | useEffect(() => {
15 | const updateDimensions = () => {
16 | if (containerRef.current) {
17 | setDimensions({
18 | width: containerRef.current.clientWidth,
19 | height: containerRef.current.clientHeight,
20 | })
21 | }
22 | }
23 |
24 | updateDimensions()
25 | window.addEventListener('resize', updateDimensions)
26 | return () => window.removeEventListener('resize', updateDimensions)
27 | }, [])
28 |
29 | const columnWidth = dimensions.width / columns
30 |
31 | const lines = useMemo(() => {
32 | return Array.from({ length: columns }, (_, i) => {
33 | const x = `${(i / columns) * 100}%`
34 | return { x, key: i }
35 | })
36 | }, [columns])
37 |
38 | return (
39 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {lines.map(({ x, key }) => (
51 |
52 |
61 |
62 |
63 | ))}
64 |
65 |
66 | )
67 | }
68 |
69 | interface AnimatedLineProps {
70 | x: string
71 | }
72 |
73 | const AnimatedLine: React.FC = ({ x }) => {
74 | const duration = 3 + Math.random() * 2 // Random duration between 3-5 seconds
75 | const delay = Math.random() * 5 // Random delay up to 5 seconds
76 |
77 | return (
78 |
94 | )
95 | }
96 |
97 | export default LineAnimation
98 |
--------------------------------------------------------------------------------
/apps/web/components/deploy/deploy-info-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { getDatabaseUrl, getPoolerUrl } from '@database.build/deploy/supabase'
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogDescription,
8 | DialogHeader,
9 | DialogTitle,
10 | } from '~/components/ui/dialog'
11 | import { DeployedDatabase } from '~/data/deployed-databases/deployed-databases-query'
12 | import { SupabaseIcon } from '../supabase-icon'
13 | import { Button } from '../ui/button'
14 | import { SupabaseDeployInfo, SupabaseDeploymentInfo } from './deploy-info'
15 |
16 | type SupabaseProject = {
17 | id: string
18 | name: string
19 | pooler: {
20 | host: string
21 | name: string
22 | port: number
23 | user: string
24 | }
25 | region: string
26 | database: {
27 | host: string
28 | name: string
29 | port: number
30 | user: string
31 | }
32 | createdAt: string
33 | organizationId: string
34 | }
35 |
36 | export type DeployInfoDialogProps = {
37 | deployedDatabase: DeployedDatabase
38 | open: boolean
39 | onOpenChange: (open: boolean) => void
40 | onRedeploy: () => void
41 | }
42 |
43 | export function DeployInfoDialog({
44 | deployedDatabase,
45 | open,
46 | onOpenChange,
47 | onRedeploy,
48 | }: DeployInfoDialogProps) {
49 | const { project } = deployedDatabase.provider_metadata as { project: SupabaseProject }
50 |
51 | const projectUrl = `${process.env.NEXT_PUBLIC_SUPABASE_PLATFORM_URL}/dashboard/project/${project.id}`
52 | const databaseUrl = getDatabaseUrl({ project })
53 | const poolerUrl = getPoolerUrl({ project })
54 |
55 | const deployInfo: SupabaseDeploymentInfo = {
56 | name: project.name,
57 | url: projectUrl,
58 | databaseUrl,
59 | poolerUrl,
60 | createdAt: deployedDatabase.last_deployment_at
61 | ? new Date(deployedDatabase.last_deployment_at)
62 | : undefined,
63 | }
64 |
65 | return (
66 |
67 |
68 |
69 |
70 |
71 | Database deployed to Supabase
72 |
73 |
74 |
75 |
76 |
77 |
78 | If you wish to redeploy your latest in-browser database to Supabase, click{' '}
79 | Redeploy .
80 |
81 |
{
83 | onOpenChange(false)
84 | onRedeploy()
85 | }}
86 | >
87 | Redeploy
88 |
89 |
90 |
91 |
92 |
93 |
94 | )
95 | }
96 |
--------------------------------------------------------------------------------
/packages/deploy/src/supabase/get-access-token.ts:
--------------------------------------------------------------------------------
1 | import { DeployError, IntegrationRevokedError } from '../error.js'
2 | import type { Credentials, SupabaseClient, SupabasePlatformConfig } from './types.js'
3 |
4 | /**
5 | * Get the access token for a given Supabase integration.
6 | */
7 | export async function getAccessToken(
8 | ctx: {
9 | supabaseAdmin: SupabaseClient
10 | supabasePlatformConfig: SupabasePlatformConfig
11 | },
12 | params: {
13 | integrationId: number
14 | credentialsSecretId: string
15 | }
16 | ): Promise {
17 | const credentialsSecret = await ctx.supabaseAdmin.rpc('read_secret', {
18 | secret_id: params.credentialsSecretId,
19 | })
20 |
21 | if (credentialsSecret.error) {
22 | throw new DeployError('Failed to read credentials secret', { cause: credentialsSecret.error })
23 | }
24 |
25 | const credentials = JSON.parse(credentialsSecret.data) as Credentials
26 |
27 | let accessToken = credentials.accessToken
28 |
29 | // if the token expires in less than 1 hour, refresh it
30 | if (new Date(credentials.expiresAt) < new Date(Date.now() + 1 * 60 * 60 * 1000)) {
31 | const now = Date.now()
32 |
33 | const newCredentialsResponse = await fetch(
34 | `${ctx.supabasePlatformConfig.apiUrl}/v1/oauth/token`,
35 | {
36 | method: 'POST',
37 | headers: {
38 | 'Content-Type': 'application/x-www-form-urlencoded',
39 | Accept: 'application/json',
40 | Authorization: `Basic ${btoa(`${ctx.supabasePlatformConfig.oauthClientId}:${ctx.supabasePlatformConfig.oauthSecret}`)}`,
41 | },
42 | body: new URLSearchParams({
43 | grant_type: 'refresh_token',
44 | refresh_token: credentials.refreshToken,
45 | }),
46 | }
47 | )
48 |
49 | if (!newCredentialsResponse.ok) {
50 | if (newCredentialsResponse.status === 406) {
51 | throw new IntegrationRevokedError()
52 | }
53 |
54 | throw new DeployError('Failed to fetch new credentials', {
55 | cause: {
56 | status: newCredentialsResponse.status,
57 | statusText: newCredentialsResponse.statusText,
58 | },
59 | })
60 | }
61 |
62 | const newCredentials = (await newCredentialsResponse.json()) as {
63 | access_token: string
64 | refresh_token: string
65 | expires_in: number
66 | }
67 |
68 | accessToken = newCredentials.access_token
69 |
70 | const expiresAt = new Date(now + newCredentials.expires_in * 1000)
71 |
72 | const updateCredentialsSecret = await ctx.supabaseAdmin.rpc('update_secret', {
73 | secret_id: params.credentialsSecretId,
74 | new_secret: JSON.stringify({
75 | accessToken: newCredentials.access_token,
76 | expiresAt: expiresAt.toISOString(),
77 | refreshToken: newCredentials.refresh_token,
78 | }),
79 | })
80 |
81 | if (updateCredentialsSecret.error) {
82 | throw new DeployError('Failed to update credentials secret', {
83 | cause: updateCredentialsSecret.error,
84 | })
85 | }
86 | }
87 |
88 | return accessToken
89 | }
90 |
--------------------------------------------------------------------------------
/apps/web/components/layout/header/deploy-button/deploy-supabase-tab.tsx:
--------------------------------------------------------------------------------
1 | import { generateProjectName } from '@database.build/deploy/supabase'
2 | import { Loader } from 'lucide-react'
3 | import { useRouter } from 'next/navigation'
4 | import { PropsWithChildren } from 'react'
5 | import { SchemaOverlapWarning } from '~/components/deploy/schema-overlap-warning'
6 | import { SupabaseIcon } from '~/components/supabase-icon'
7 | import { Button } from '~/components/ui/button'
8 | import { TabsContent } from '~/components/ui/tabs'
9 | import { useIntegrationQuery } from '~/data/integrations/integration-query'
10 | import type { MergedDatabase } from '~/data/merged-databases/merged-database'
11 | import { getDeployUrl } from '~/lib/util'
12 |
13 | export function DeploySupabaseTab(props: { database: MergedDatabase }) {
14 | const router = useRouter()
15 | const { data: integration, isLoading: isLoadingIntegration } = useIntegrationQuery('Supabase')
16 |
17 | const handleDeployClick = () => {
18 | const deployUrl = getDeployUrl({
19 | databaseId: props.database.id,
20 | integrationId: integration!.id,
21 | })
22 |
23 | router.push(deployUrl)
24 | }
25 |
26 | return (
27 |
28 | Deploy to Supabase
29 | {!integration ? (
30 |
35 | ) : (
36 |
37 | You are about to deploy your in-browser database to Supabase. This will create a new
38 | Supabase project under your linked organization.
39 |
43 |
44 |
45 | )}
46 |
53 | Deploy
54 |
55 |
56 | )
57 | }
58 |
59 | type DeployCardProps = {
60 | organization: { id: string; name: string }
61 | projectName: string
62 | }
63 |
64 | function DeployCard({ organization, projectName }: DeployCardProps) {
65 | return (
66 |
67 | Organization
68 |
69 |
74 | {organization.name}
75 | {' '}
76 | ({organization.id})
77 |
78 | Project name
79 | {projectName}
80 |
81 | )
82 | }
83 |
--------------------------------------------------------------------------------
/apps/web/components/deploy/redeploy-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { TriangleAlert } from 'lucide-react'
4 | import { useState } from 'react'
5 | import {
6 | Dialog,
7 | DialogContent,
8 | DialogDescription,
9 | DialogFooter,
10 | DialogHeader,
11 | DialogTitle,
12 | } from '~/components/ui/dialog'
13 | import { Input } from '~/components/ui/input'
14 | import { Button } from '../ui/button'
15 | import { SupabaseIcon } from '../supabase-icon'
16 | import { SchemaOverlapWarning } from './schema-overlap-warning'
17 | import type { MergedDatabase } from '~/data/merged-databases/merged-database'
18 |
19 | export type RedeployDialogProps = {
20 | database: MergedDatabase
21 | open: boolean
22 | onOpenChange: (open: boolean) => void
23 | onConfirm: () => void
24 | }
25 |
26 | export function RedeployDialog({ database, open, onOpenChange, onConfirm }: RedeployDialogProps) {
27 | const [confirmedValue, setConfirmedValue] = useState('')
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
35 | Confirm redeploy of {database.name}
36 |
37 |
38 |
39 |
40 | This action cannot be undone.
41 |
42 |
43 | Redeploying will completely overwrite the existing deployed database with the latest
44 | version of your browser database. Existing schema and data in the deployed database
45 | will be lost.
46 |
47 |
48 |
49 |
50 |
51 | Type {database.name} to confirm.
52 |
53 |
setConfirmedValue(e.target.value)}
58 | />
59 |
60 |
61 |
62 |
63 | {
66 | onOpenChange(false)
67 | }}
68 | >
69 | Cancel
70 |
71 | {
74 | onConfirm()
75 | }}
76 | disabled={confirmedValue !== database.name}
77 | >
78 | I understand, overwrite the database
79 |
80 |
81 |
82 |
83 | )
84 | }
85 |
--------------------------------------------------------------------------------
/apps/web/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { ChevronRight, MoreHorizontal } from "lucide-react"
4 |
5 | import { cn } from "~/lib/utils"
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<"nav"> & {
10 | separator?: React.ReactNode
11 | }
12 | >(({ ...props }, ref) => )
13 | Breadcrumb.displayName = "Breadcrumb"
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<"ol">
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | BreadcrumbList.displayName = "BreadcrumbList"
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<"li">
33 | >(({ className, ...props }, ref) => (
34 |
39 | ))
40 | BreadcrumbItem.displayName = "BreadcrumbItem"
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<"a"> & {
45 | asChild?: boolean
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : "a"
49 |
50 | return (
51 |
56 | )
57 | })
58 | BreadcrumbLink.displayName = "BreadcrumbLink"
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<"span">
63 | >(({ className, ...props }, ref) => (
64 |
72 | ))
73 | BreadcrumbPage.displayName = "BreadcrumbPage"
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<"li">) => (
80 | svg]:w-3.5 [&>svg]:h-3.5", className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | )
89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"span">) => (
95 |
101 |
102 | More
103 |
104 | )
105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | }
116 |
--------------------------------------------------------------------------------
/apps/web/components/deploy/deploy-info.tsx:
--------------------------------------------------------------------------------
1 | import { format } from 'date-fns'
2 | import Link from 'next/link'
3 | import { CopyableField } from '~/components/copyable-field'
4 | import { Badge } from '~/components/ui/badge'
5 |
6 | export type SupabaseDeploymentInfo = {
7 | name: string
8 | url: string
9 | databasePassword?: string
10 | databaseUrl: string
11 | poolerUrl: string
12 | createdAt?: Date
13 | }
14 |
15 | export type DeployInfoProps = {
16 | info: SupabaseDeploymentInfo
17 | isRedeploy?: boolean
18 | }
19 |
20 | export function SupabaseDeployInfo({ info, isRedeploy = false }: DeployInfoProps) {
21 | const deployText = isRedeploy ? 'redeployed' : 'deployed'
22 |
23 | return (
24 |
25 |
26 | Your in-browser database was {deployText} to the Supabase project{' '}
27 |
33 | {info.name}
34 |
35 | {info.createdAt
36 | ? ` at ${format(info.createdAt, 'h:mm a')} on ${format(info.createdAt, 'MMMM d, yyyy')}`
37 | : ''}
38 | .
39 |
40 |
41 |
44 | Database Connection URL{' '}
45 |
46 | IPv6
47 |
48 |
49 | }
50 | value={info.databaseUrl}
51 | />
52 |
55 | Pooler Connection URL{' '}
56 |
57 |
58 | IPv4
59 |
60 |
61 | IPv6
62 |
63 |
64 |
65 | }
66 | value={info.poolerUrl}
67 | />
68 | {info.databasePassword && (
69 | <>
70 |
71 |
72 | Please{' '}
73 |
74 | save your database password securely
75 | {' '}
76 | as it won't be displayed again.
77 |
78 | >
79 | )}
80 |
81 | You can change your password and learn more about your connection strings in your{' '}
82 |
88 | database settings
89 |
90 | .
91 |
92 |
93 |
94 | )
95 | }
96 |
--------------------------------------------------------------------------------
/apps/web/components/ai-icon-animation/ai-icon-animation.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | /**
4 | * Copied from supabase/supabase ui package.
5 | */
6 |
7 | import { useState, useEffect } from 'react'
8 | import styles from './ai-icon-animation-style.module.css'
9 | import { cn } from '~/lib/utils'
10 |
11 | interface Props {
12 | loading?: boolean
13 | className?: string
14 | allowHoverEffect?: boolean
15 | }
16 |
17 | const AiIconAnimation = ({ loading = false, className, allowHoverEffect = false }: Props) => {
18 | const [step, setStep] = useState(1)
19 | const [exitStep, setExitStep] = useState(1)
20 | const [isAnimating, setIsAnimating] = useState(true)
21 |
22 | useEffect(() => {
23 | const interval = setInterval(() => {
24 | if (loading) {
25 | setIsAnimating(true)
26 | setStep((step) => {
27 | return (step % 5) + 1
28 | })
29 | setExitStep((step) => {
30 | return (step % 5) + 1
31 | })
32 | }
33 | }, 500)
34 | return () => clearInterval(interval)
35 | }, [loading])
36 |
37 | useEffect(() => {
38 | if (loading === false) {
39 | setTimeout(() => {
40 | setIsAnimating(false)
41 | }, 500)
42 | setStep(1)
43 | }
44 | }, [loading])
45 |
46 | return (
47 |
98 | )
99 | }
100 |
101 | export { AiIconAnimation }
102 |
--------------------------------------------------------------------------------
/apps/web/config/default-colors.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copied from supabase/supabase config package.
3 | */
4 |
5 | module.exports = {
6 | brand: {
7 | brand1: 'hsla(153, 80%, 99%, 1)',
8 | brand2: 'hsla(148, 88%, 97%, 1)',
9 | brand3: 'hsla(148, 76%, 95%, 1)',
10 | brand4: 'hsla(148, 68%, 92%, 1)',
11 | brand5: 'hsla(148, 59%, 88%, 1)',
12 | brand6: 'hsla(149, 43%, 82%, 1)',
13 | brand7: 'hsla(149, 43%, 69%, 1)',
14 | brand8: 'hsla(154, 55%, 45%, 1)',
15 | brand9: 'hsla(153, 60%, 53%, 1)',
16 | brand10: 'hsla(153, 50%, 50%, 1)',
17 | brand11: 'hsla(153, 50%, 34%, 1)',
18 | brand12: 'hsla(153, 40%, 12%, 1)',
19 | },
20 | brandDark: {
21 | brand1: 'hsla(153, 75%, 6%, 1)',
22 | brand2: 'hsla(153, 73%, 7%, 1)',
23 | brand3: 'hsla(154, 69%, 9%, 1)',
24 | brand4: 'hsla(154, 67%, 11%, 1)',
25 | brand5: 'hsla(154, 66%, 13%, 1)',
26 | brand6: 'hsla(154, 64%, 17%, 1)',
27 | brand7: 'hsla(154, 62%, 22%, 1)',
28 | brand8: 'hsla(153, 60%, 28%, 1)',
29 | brand9: 'hsla(153, 60%, 53%, 1)',
30 | brand10: 'hsla(153, 60%, 70%, 1)',
31 | brand11: 'hsla(153, 60%, 50%, 1)',
32 | brand12: 'hsla(153, 60%, 95%, 1)',
33 | },
34 | contrast: {
35 | 'brand-hiContrast': 'hsl(var(--brand-default))',
36 | 'brand-loContrast': 'hsl(var(--brand-300))',
37 | },
38 | scale: {
39 | scale1: 'var(--colors-slate1)',
40 | scale2: 'var(--colors-slate2)',
41 | scale3: 'var(--colors-slate3)',
42 | scale4: 'var(--colors-slate4)',
43 | scale5: 'var(--colors-slate5)',
44 | scale6: 'var(--colors-slate6)',
45 | scale7: 'var(--colors-slate7)',
46 | scale8: 'var(--colors-slate8)',
47 | scale9: 'var(--colors-slate9)',
48 | scale10: 'var(--colors-slate10)',
49 | scale11: 'var(--colors-slate11)',
50 | scale12: 'var(--colors-slate12)',
51 | },
52 | scaleA: {
53 | scaleA1: 'var(--colors-slateA1)',
54 | scaleA2: 'var(--colors-slateA2)',
55 | scaleA3: 'var(--colors-slateA3)',
56 | scaleA4: 'var(--colors-slateA4)',
57 | scaleA5: 'var(--colors-slateA5)',
58 | scaleA6: 'var(--colors-slateA6)',
59 | scaleA7: 'var(--colors-slateA7)',
60 | scaleA8: 'var(--colors-slateA8)',
61 | scaleA9: 'var(--colors-slateA9)',
62 | scaleA10: 'var(--colors-slateA10)',
63 | scaleA11: 'var(--colors-slateA11)',
64 | scaleA12: 'var(--colors-slateA12)',
65 | },
66 | scaleDark: {
67 | scale1: 'var(--colors-gray1)',
68 | scale2: 'var(--colors-gray2)',
69 | scale3: 'var(--colors-gray3)',
70 | scale4: 'var(--colors-gray4)',
71 | scale5: 'var(--colors-gray5)',
72 | scale6: 'var(--colors-gray6)',
73 | scale7: 'var(--colors-gray7)',
74 | scale8: 'var(--colors-gray8)',
75 | scale9: 'var(--colors-gray9)',
76 | scale10: 'var(--colors-gray10)',
77 | scale11: '#bbbbbb',
78 | scale12: 'var(--colors-gray12)',
79 | },
80 | scaleADark: {
81 | scaleA1: 'var(--colors-grayA1)',
82 | scaleA2: 'var(--colors-grayA2)',
83 | scaleA3: 'var(--colors-grayA3)',
84 | scaleA4: 'var(--colors-grayA4)',
85 | scaleA5: 'var(--colors-grayA5)',
86 | scaleA6: 'var(--colors-grayA6)',
87 | scaleA7: 'var(--colors-grayA7)',
88 | scaleA8: 'var(--colors-grayA8)',
89 | scaleA9: 'var(--colors-grayA9)',
90 | scaleA10: 'var(--colors-grayA10)',
91 | scaleA11: 'var(--colors-grayA11)',
92 | scaleA12: 'var(--colors-grayA12)',
93 | },
94 | }
95 |
--------------------------------------------------------------------------------
/apps/deploy-worker/src/index.ts:
--------------------------------------------------------------------------------
1 | import { DeployError, IntegrationRevokedError } from '@database.build/deploy'
2 | import {
3 | type Database,
4 | type Region,
5 | type SupabaseDeploymentConfig,
6 | type SupabasePlatformConfig,
7 | } from '@database.build/deploy/supabase'
8 | import { revokeIntegration } from '@database.build/deploy/supabase'
9 | import { serve } from '@hono/node-server'
10 | import { zValidator } from '@hono/zod-validator'
11 | import { createClient } from '@supabase/supabase-js'
12 | import { Hono } from 'hono'
13 | import { cors } from 'hono/cors'
14 | import { HTTPException } from 'hono/http-exception'
15 | import { z } from 'zod'
16 | import { deploy } from './deploy.ts'
17 |
18 | const supabasePlatformConfig: SupabasePlatformConfig = {
19 | url: process.env.SUPABASE_PLATFORM_URL!,
20 | apiUrl: process.env.SUPABASE_PLATFORM_API_URL!,
21 | oauthClientId: process.env.SUPABASE_OAUTH_CLIENT_ID!,
22 | oauthSecret: process.env.SUPABASE_OAUTH_SECRET!,
23 | }
24 |
25 | const supabaseDeploymentConfig: SupabaseDeploymentConfig = {
26 | region: process.env.SUPABASE_PLATFORM_DEPLOY_REGION! as Region,
27 | }
28 |
29 | const app = new Hono()
30 |
31 | app.use('*', cors())
32 |
33 | app.post(
34 | '/',
35 | zValidator(
36 | 'json',
37 | z.object({
38 | databaseId: z.string(),
39 | integrationId: z.number().int(),
40 | databaseUrl: z.string(),
41 | })
42 | ),
43 | async (c) => {
44 | const { databaseId, integrationId, databaseUrl: localDatabaseUrl } = c.req.valid('json')
45 |
46 | const accessToken = c.req.header('Authorization')?.replace('Bearer ', '')
47 | const refreshToken = c.req.header('X-Refresh-Token')
48 | if (!accessToken || !refreshToken) {
49 | throw new HTTPException(401, { message: 'Unauthorized' })
50 | }
51 |
52 | const supabaseAdmin = createClient(
53 | process.env.SUPABASE_URL!,
54 | process.env.SUPABASE_SERVICE_ROLE_KEY!
55 | )
56 |
57 | const supabase = createClient(
58 | process.env.SUPABASE_URL!,
59 | process.env.SUPABASE_ANON_KEY!
60 | )
61 |
62 | const { error } = await supabase.auth.setSession({
63 | access_token: accessToken,
64 | refresh_token: refreshToken,
65 | })
66 |
67 | if (error) {
68 | throw new HTTPException(401, { message: 'Unauthorized' })
69 | }
70 |
71 | const ctx = {
72 | supabase,
73 | supabaseAdmin,
74 | supabasePlatformConfig,
75 | supabaseDeploymentConfig,
76 | }
77 |
78 | try {
79 | const project = await deploy(ctx, { databaseId, integrationId, localDatabaseUrl })
80 | return c.json({ project })
81 | } catch (error: unknown) {
82 | console.error(error)
83 | if (error instanceof DeployError) {
84 | throw new HTTPException(500, { message: error.message })
85 | }
86 | if (error instanceof IntegrationRevokedError) {
87 | await revokeIntegration(ctx, { integrationId })
88 | throw new HTTPException(406, { message: error.message })
89 | }
90 | throw new HTTPException(500, { message: 'Internal server error' })
91 | }
92 | }
93 | )
94 |
95 | app.get('')
96 |
97 | const port = 4000
98 | console.log(`Server is running on port ${port}`)
99 |
100 | serve({
101 | fetch: app.fetch,
102 | port,
103 | })
104 |
--------------------------------------------------------------------------------