├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── README.md ├── bun.lockb ├── package.json ├── src ├── auth │ └── Dockerfile ├── database │ ├── Dockerfile │ └── pg_hba.conf ├── index.ts ├── kong │ ├── Dockerfile │ └── kong.yml ├── pg-rest │ └── Dockerfile └── studio │ └── Dockerfile ├── supabased.conf └── tsconfig.json /.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 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | # vercel 36 | .vercel 37 | 38 | dist/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "apps/pg-meta2"] 2 | path = apps/pg-meta2 3 | url = https://github.com/supabase/postgres-meta 4 | [submodule "apps/pg-meta"] 5 | path = apps/pg-meta 6 | url = https://github.com/supabase/postgres-meta 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["Dockerfiles"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SupaFly 2 | 3 | This easiest way to deploy Supabase to Fly.io 4 | 5 | 6 | 7 | ## Features 8 | 9 | - Uses new [Fly.io Apps v2](https://fly.io/docs/reference/apps/) platform 10 | - Uses Fly's [private networking](https://fly.io/docs/reference/private-networking/) 11 | > Applications within the same organization are assigned special addresses ("6PN addresses") tied to the organization. Those applications can talk to each other because of those 6PN addresses, but applications from other organizations can't; the Fly platform won't forward between different 6PN networks. 12 | - Turborepo to make it easy to build off of 13 | 14 | ## Prerequisites 15 | 16 | Run the following command: 17 | 18 | - [x] Fly.io CLI tool installed 19 | - [x] Wireguard installed 20 | - [x] Add card to Fly.io account / organazation 21 | 22 | ### Install [flyctl](https://fly.io/docs/hands-on/install-flyctl/), or the Fly.io CLI tool 23 | 24 | ```sh 25 | brew install flyctl 26 | ``` 27 | 28 | ### [Wireguard Installation Instructions](https://www.wireguard.com/install/) 29 | 30 | ### We recommend creating a new organization for your SupaFly project 31 | 32 | > If you don't pass in a value for `--org` it will default to your personal organization 33 | 34 | ## Deploying SupaFly 35 | 36 | ```sh 37 | npx supafly@latest 38 | ``` 39 | 40 | PNG image 41 | 42 | ## Infrastructure Deployed 43 | 44 | - Supabase flavor, postgres database 45 | - [Postgres-meta](https://github.com/supabase/postgres-meta) 46 | - [Supabase Auth Service](https://github.com/supabase/auth-helpers) 47 | - [PostgREST](https://github.com/PostgREST/postgrest) 48 | - [Api Gateway (kong)](https://docs.konghq.com/gateway/latest/production/deployment-topologies/db-less-and-declarative-config/) 49 | - [Supabase Studio](https://github.com/supabase/supabase/tree/master/studio) 50 | 51 | ## Things to work on 52 | 53 | - Supabase Storage Deployment 54 | - Postgrest alpine image 55 | - issues persisting data with database redeployments 56 | - Pass in passwords for postgres roles 57 | - Better name generation for Supabase services 58 | 59 | [SupaFly Progress Tracker](https://github.com/users/nicholasoxford/projects/1/views/1) 60 | 61 | ## How I created this 62 | 63 | - Got inspired by @kiwicopple [reddit comment](https://www.reddit.com/r/Supabase/comments/s9rdfd/globally_distributed_postgres_with_supabase/) about deploying Supabase DB to Fly 64 | - Took the Supabase [docker-compose](https://github.com/supabase/supabase/blob/master/docker/docker-compose.yml) file and created fly services for each 65 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholasoxford/SupaFly/4cbe216b7f585c472f77826180e4646c810ae8ca/bun.lockb -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supafly", 3 | "version": "1.0.10", 4 | "description": "Supafly is a CLI tool for deploying Supabase to fly.io", 5 | "keywords": [ 6 | "supabase", 7 | "fly.io", 8 | "supafly" 9 | ], 10 | "license": "ISC", 11 | "author": "@nicholsaoxford", 12 | "type": "module", 13 | "main": "dist/index.js", 14 | "bin": { 15 | "supa": "dist/index.js" 16 | }, 17 | "files": [ 18 | "dist", 19 | "src/auth/Dockerfile", 20 | "src/database/Dockerfile", 21 | "src/database/pg_hba.conf", 22 | "src/kong/Dockerfile", 23 | "src/kong/kong.yml", 24 | "src/pg-rest/Dockerfile", 25 | "src/studio/Dockerfile" 26 | ], 27 | "scripts": { 28 | "build": "bun build src/index.ts --target=node --outfile=dist/index.js", 29 | "start": "bun dist/index.js", 30 | "skip": "bun src/index.ts -y -r lax --org supabased ", 31 | "dev": "bun --watch src/index.ts", 32 | "test": "echo \"Error: no test specified\" && exit 1" 33 | }, 34 | "dependencies": { 35 | "@inquirer/prompts": "^5.3.8", 36 | "chalk": "^5.3.0", 37 | "commander": "^12.1.0", 38 | "figlet": "^1.7.0", 39 | "fly-admin": "^1.6.1", 40 | "njwt": "^2.0.1", 41 | "ora": "^8.0.1", 42 | "pg": "^8.12.0", 43 | "random-words": "^2.0.0", 44 | "secure-random": "^1.1.2" 45 | }, 46 | "devDependencies": { 47 | "@types/figlet": "^1.5.8", 48 | "@types/node": "^20.10.5", 49 | "@types/pg": "^8.11.5", 50 | "@types/secure-random": "^1.1.3", 51 | "typescript": "^5.5.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/auth/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM supabase/gotrue 2 | 3 | EXPOSE 9999 4 | 5 | -------------------------------------------------------------------------------- /src/database/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use a specific version of the Supabase PostgreSQL base image 2 | FROM supabase/postgres:15.1.0.70 3 | 4 | # Set a non-default username for the PostgreSQL user 5 | ENV POSTGRES_PASSWORD=password 6 | 7 | # Set environment variables for the database name and port 8 | ENV POSTGRES_PORT=5432 9 | 10 | # Expose the PostgreSQL port 11 | EXPOSE $POSTGRES_PORT 12 | 13 | # Set up a volume to persist the PostgreSQL data 14 | VOLUME /var/lib/postgresql/data 15 | COPY ./pg_hba.conf /etc/postgresql/pg_hba.conf 16 | 17 | 18 | # Start the PostgreSQL server 19 | CMD ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"] -------------------------------------------------------------------------------- /src/database/pg_hba.conf: -------------------------------------------------------------------------------- 1 | # PostgreSQL Client Authentication Configuration File 2 | # =================================================== 3 | # 4 | # Refer to the "Client Authentication" section in the PostgreSQL 5 | # documentation for a complete description of this file. A short 6 | # synopsis follows. 7 | # 8 | # This file controls: which hosts are allowed to connect, how clients 9 | # are authenticated, which PostgreSQL user names they can use, which 10 | # databases they can access. Records take one of these forms: 11 | # 12 | # local DATABASE USER METHOD [OPTIONS] 13 | # host DATABASE USER ADDRESS METHOD [OPTIONS] 14 | # hostssl DATABASE USER ADDRESS METHOD [OPTIONS] 15 | # hostnossl DATABASE USER ADDRESS METHOD [OPTIONS] 16 | # hostgssenc DATABASE USER ADDRESS METHOD [OPTIONS] 17 | # hostnogssenc DATABASE USER ADDRESS METHOD [OPTIONS] 18 | # 19 | # (The uppercase items must be replaced by actual values.) 20 | # 21 | # The first field is the connection type: "local" is a Unix-domain 22 | # socket, "host" is either a plain or SSL-encrypted TCP/IP socket, 23 | # "hostssl" is an SSL-encrypted TCP/IP socket, and "hostnossl" is a 24 | # non-SSL TCP/IP socket. Similarly, "hostgssenc" uses a 25 | # GSSAPI-encrypted TCP/IP socket, while "hostnogssenc" uses a 26 | # non-GSSAPI socket. 27 | # 28 | # DATABASE can be "all", "sameuser", "samerole", "replication", a 29 | # database name, or a comma-separated list thereof. The "all" 30 | # keyword does not match "replication". Access to replication 31 | # must be enabled in a separate record (see example below). 32 | # 33 | # USER can be "all", a user name, a group name prefixed with "+", or a 34 | # comma-separated list thereof. In both the DATABASE and USER fields 35 | # you can also write a file name prefixed with "@" to include names 36 | # from a separate file. 37 | # 38 | # ADDRESS specifies the set of hosts the record matches. It can be a 39 | # host name, or it is made up of an IP address and a CIDR mask that is 40 | # an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that 41 | # specifies the number of significant bits in the mask. A host name 42 | # that starts with a dot (.) matches a suffix of the actual host name. 43 | # Alternatively, you can write an IP address and netmask in separate 44 | # columns to specify the set of hosts. Instead of a CIDR-address, you 45 | # can write "samehost" to match any of the server's own IP addresses, 46 | # or "samenet" to match any address in any subnet that the server is 47 | # directly connected to. 48 | # 49 | # METHOD can be "trust", "reject", "md5", "password", "scram-sha-256", 50 | # "gss", "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert". 51 | # Note that "password" sends passwords in clear text; "md5" or 52 | # "scram-sha-256" are preferred since they send encrypted passwords. 53 | # 54 | # OPTIONS are a set of options for the authentication in the format 55 | # NAME=VALUE. The available options depend on the different 56 | # authentication methods -- refer to the "Client Authentication" 57 | # section in the documentation for a list of which options are 58 | # available for which authentication methods. 59 | # 60 | # Database and user names containing spaces, commas, quotes and other 61 | # special characters must be quoted. Quoting one of the keywords 62 | # "all", "sameuser", "samerole" or "replication" makes the name lose 63 | # its special character, and just match a database or username with 64 | # that name. 65 | # 66 | # This file is read on server startup and when the server receives a 67 | # SIGHUP signal. If you edit the file on a running system, you have to 68 | # SIGHUP the server for the changes to take effect, run "pg_ctl reload", 69 | # or execute "SELECT pg_reload_conf()". 70 | # 71 | # Put your actual configuration here 72 | # ---------------------------------- 73 | # 74 | # If you want to allow non-local connections, you need to add more 75 | # "host" records. In that case you will also need to make PostgreSQL 76 | # listen on a non-local interface via the listen_addresses 77 | # configuration parameter, or via the -i or -h command line switches. 78 | 79 | # TYPE DATABASE USER ADDRESS METHOD 80 | 81 | # trust local connections 82 | local all supabase_admin scram-sha-256 83 | local all all peer map=supabase_map 84 | host all all 127.0.0.1/32 trust 85 | host all all ::1/128 trust 86 | 87 | # IPv4 external connections 88 | host all all 10.0.0.0/8 scram-sha-256 89 | host all all 172.16.0.0/12 scram-sha-256 90 | host all all 192.168.0.0/16 scram-sha-256 91 | host all all 0.0.0.0/0 scram-sha-256 92 | 93 | host all all fdaa::/16 md5 94 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import { spawn, ChildProcessWithoutNullStreams } from "child_process"; 4 | import figlet from "figlet"; 5 | import { Command } from "commander"; 6 | import { select, confirm, input } from "@inquirer/prompts"; 7 | import chalk from "chalk"; 8 | import ora from "ora"; 9 | import njwt from "njwt"; 10 | import secureRandom from "secure-random"; 11 | import { readFile, writeFile } from "fs/promises"; 12 | import { generate } from "random-words"; 13 | 14 | // Create cli program helper and options 15 | const program = new Command(); 16 | program 17 | .version("0.0.1") 18 | .description("🚀 Deploy Supabase to Fly.io 🌐") 19 | .option("-d, --dir [value]", "📁 Specify the directory for deployment") 20 | .option("-O, --org [value]", "🎯 Fly.io Target Organization") 21 | .option("-r, --region [value]", "🌍 Fly.io Target Region") 22 | .option("--dbUrl [value]", "🔗 Existing Database URL") 23 | .option("-y, --yes", "✅ Skip prompts and deploy") 24 | .parse(process.argv); 25 | const options = program.opts(); 26 | 27 | // Globally available info variable 28 | let global: GlobalInfo = { 29 | username: "", 30 | defaultRegion: options.region ?? "", 31 | organization: options.org ?? "", 32 | jwtTokens: { 33 | anonToken: "", 34 | serviceToken: "", 35 | }, 36 | pgMeta: { 37 | ipv6: "", 38 | }, 39 | pgRest: { 40 | ipv6: "", 41 | }, 42 | pgAuth: { 43 | ipv6: "", 44 | }, 45 | database: { 46 | ipv6: "", 47 | name: "", 48 | }, 49 | kong: { 50 | ipv6: "", 51 | publicUrl: "", 52 | }, 53 | studio: { 54 | ipv6: "", 55 | publicUrl: "", 56 | }, 57 | defaultArgs: [], 58 | directory: options.dir ?? "", 59 | dbPath: "src/database", 60 | pgRestPath: "src/pg-rest", 61 | authPath: "src/auth", 62 | studioPath: "src/studio", 63 | kongPath: "src/kong", 64 | metaPath: "src/pg-meta", 65 | yes: Boolean(options.yes), 66 | FLY_API_TOKEN: "", 67 | }; 68 | 69 | main(); 70 | 71 | // Deploy supabase starter kit to fly.io 72 | async function main() { 73 | // Cool CLI font when starting CLI tool 74 | console.log(figlet.textSync("Supa", "Larry 3D")); 75 | console.log(figlet.textSync("Fly", "Larry 3D")); 76 | 77 | // check if fly cli is authenticated 78 | await flyAuth(); 79 | 80 | await createDirectories(); 81 | 82 | // chose default region if not passed in 83 | await flySetDefaultRegion(); 84 | 85 | // set default org if passed in 86 | await flySetDefaultOrg(); 87 | 88 | // turn our info object into default fly args 89 | setDefaultFlyArgs(); 90 | 91 | // deploy database 92 | await flyDeployAndPrepareDB(); 93 | 94 | // deploy api 95 | await deployPGMeta(); 96 | 97 | // deploy postGREST 98 | await deployPGREST(); 99 | 100 | // generate service and anon tokens 101 | generateSupaJWTs(); 102 | 103 | await deployAuth(); 104 | 105 | await deployCleanUp(); 106 | 107 | await deployKong(); 108 | 109 | await apiGatewayTest(); 110 | 111 | await deployStudio(); 112 | 113 | await studioTest(); 114 | } 115 | 116 | async function flyAuth() { 117 | const authSpinner = ora({ 118 | text: `Checking fly cli authorization...`, 119 | color: "yellow", 120 | }).start(); 121 | let username = await whoami(); 122 | if (!username) { 123 | // async shell cmd 124 | authSpinner.stop(); 125 | await flyLogin(); 126 | username = await whoami(); 127 | } 128 | 129 | // grab username 130 | authSpinner.stop(); 131 | 132 | // confirm user wants to continue 133 | const resp = await confirm({ 134 | message: `You are logged into Fly.io as: ${username}. Do you want to continue?`, 135 | default: true, 136 | }); 137 | 138 | // if they want to login 139 | if (!resp) { 140 | await flyLogin(); 141 | username = await whoami(); 142 | } 143 | 144 | // if we still dont have a username, exit 145 | if (!username) { 146 | console.error( 147 | chalk.red("You must be logged into fly.io to deploy Supabase") 148 | ); 149 | process.exit(1); 150 | } 151 | global.username = username; 152 | getFlyApiToken(); 153 | console.log("Deploying to fly.io as:", chalk.green(global.username)); 154 | } 155 | 156 | async function getFlyApiToken() { 157 | // call fly auth token 158 | // set global variable 159 | const tokenCommand = spawn("fly", ["auth", "token"]); 160 | const token = await execAsync(tokenCommand); 161 | global.FLY_API_TOKEN = token; 162 | } 163 | 164 | // Create default cli args like org and region to make life easier 165 | function setDefaultFlyArgs() { 166 | let argsArray = ["--force-machines", "--auto-confirm"]; 167 | global.defaultArgs = argsArray; 168 | return; 169 | } 170 | 171 | async function flyLogin() { 172 | const flyLoginSpawn = spawn("fly", ["auth", "login"]); 173 | return await execAsync(flyLoginSpawn); 174 | } 175 | 176 | /** 177 | * 178 | * @returns username or email of the currently logged in fly user 179 | */ 180 | async function whoami() { 181 | const whoamiSpawn = spawn("fly", ["auth", "whoami"]); 182 | return await execAsync(whoamiSpawn); 183 | } 184 | 185 | // Fly io specific functions 186 | //Deploying postgres-meta 187 | async function deployPGMeta() { 188 | let metaName; 189 | if (!global.yes) { 190 | metaName = await input({ 191 | message: 192 | "Enter a name for your postgres metadata instance, or leave blank for a generated one", 193 | }); 194 | } 195 | 196 | // git clone https://github.com/supabase/postgres-meta 197 | const gitClone = spawn("git", [ 198 | "clone", 199 | "https://github.com/supabase/postgres-meta", 200 | "./pg-meta", 201 | ]); 202 | await execAsyncLog(gitClone); 203 | 204 | const metaSpinner = ora({ 205 | text: "Creating an application Fly.io's region ${globalInfo.defaultRegion} to host your PG metadata server", 206 | color: "blue", 207 | }).start(); 208 | 209 | // if we dont have a name passed in, we need to generate one 210 | const nameCommands = metaName ? ["--name", metaName] : ["--generate-name"]; 211 | const dockerFilePath = global.metaPath + "/Dockerfile"; 212 | await updatePGMetaDockerFilePGHost(dockerFilePath, global.database.ipv6); 213 | 214 | metaSpinner.stop(); 215 | const deploySpinner = ora({ 216 | text: "Deploying postgres metadata server to Fly.io", 217 | color: "yellow", 218 | }).start(); 219 | 220 | // create array of commands 221 | const metalaunchCommandArray = ["launch"].concat( 222 | launchDefaultArgs, 223 | global.defaultArgs, 224 | nameCommands 225 | ); 226 | 227 | // run fly launch --no-deploy to allocate app 228 | global.pgMeta.ipv6 = await flyLaunchDeployInternalIPV6( 229 | metalaunchCommandArray, 230 | global.metaPath 231 | ); 232 | deploySpinner.stop(); 233 | console.log(chalk.green("Metadata deployed")); 234 | return; 235 | } 236 | 237 | async function updateFlyDBRoles(path: string) { 238 | const psqlCommand1 = `psql postgres://supabase_admin:password@localhost:5432/postgres -c "ALTER ROLE authenticator WITH PASSWORD 'password';"`; 239 | const psqlCommand2 = `psql postgres://supabase_admin:password@localhost:5432/postgres -c "ALTER ROLE supabase_auth_admin WITH PASSWORD 'password';"`; 240 | const flyProcess1 = spawn( 241 | "fly", 242 | ["ssh", "console", "--command", psqlCommand1], 243 | { 244 | cwd: path, 245 | } 246 | ); 247 | const flyProcess2 = spawn( 248 | "fly", 249 | ["ssh", "console", "--command", psqlCommand2], 250 | { 251 | cwd: path, 252 | } 253 | ); 254 | await execAsync(flyProcess1); 255 | await execAsync(flyProcess2); 256 | } 257 | async function deployStudio() { 258 | let studioName; 259 | if (!global.yes) { 260 | studioName = await input({ 261 | message: 262 | "Enter a name for your Supabase Studio instance, or leave blank for a generated one", 263 | }); 264 | } 265 | const studioSpinner = ora({ 266 | text: "Deploying Supabase Studio", 267 | color: "yellow", 268 | }).start(); 269 | // if we dont have a name passed in, we need to generate one 270 | const nameCommands = studioName 271 | ? ["--name", studioName] 272 | : ["--generate-name"]; 273 | // create array of commands 274 | const studioLaunchCommandArray = ["launch"].concat( 275 | launchDefaultArgs, 276 | global.defaultArgs, 277 | nameCommands 278 | ); 279 | 280 | const secrets = { 281 | DEFAULT_PROJECT_NAME: "SupaFly", 282 | SUPABASE_PUBLIC_URL: `https://${global.kong.publicUrl}.fly.dev`, 283 | SUPABASE_URL: `https://${global.kong.publicUrl}.fly.dev/`, 284 | STUDIO_PG_META_URL: `https://${global.kong.publicUrl}.fly.dev/pg`, 285 | SUPABASE_ANON_KEY: global.jwtTokens.anonToken, 286 | SUPABASE_SERVICE_KEY: global.jwtTokens.serviceToken, 287 | SENTRY_IGNORE_API_RESOLUTION_ERROR: 1, 288 | DEFAULT_ORGANIZATION_NAME: "SupaFly Starter Project", 289 | POSTGRES_PASSWORD: "password", 290 | LOGFLARE_URL: "https://api.logflare.app/logs", 291 | LOGFLARE_API_KEY: "2321", 292 | NEXT_PUBLIC_SITE_URL: "http://localhost:300", 293 | NEXT_PUBLIC_GOTRUE_URL: `https://${global.kong.publicUrl}.fly.dev/auth/v1`, 294 | NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001", 295 | NEXT_PUBLIC_SUPABASE_ANON_KEY: global.jwtTokens.anonToken, 296 | NEXT_PUBLIC_SUPABASE_URL: `https://${global.kong.publicUrl}.fly.dev`, 297 | }; 298 | global.kong.ipv6 = await flyLaunchDeployInternalIPV6( 299 | studioLaunchCommandArray, 300 | global.studioPath, 301 | secrets 302 | ); 303 | 304 | await allocatePublicIPs(global.studioPath); 305 | 306 | studioSpinner.stop(); 307 | console.log(chalk.green("Supabase Studio deployed")); 308 | } 309 | 310 | async function deployKong() { 311 | let kongName; 312 | if (!global.yes) { 313 | kongName = await input({ 314 | message: 315 | "Enter a name for your Kong instance, or leave blank for a generated one", 316 | }); 317 | } 318 | const kongSpinner = ora({ 319 | text: "Deploying Kong", 320 | color: "yellow", 321 | }).start(); 322 | // if we dont have a name passed in, we need to generate one 323 | const nameCommands = kongName ? ["--name", kongName] : ["--generate-name"]; 324 | 325 | // create array of commands 326 | const kongLaunchCommandArray = ["launch"].concat( 327 | launchDefaultArgs, 328 | global.defaultArgs, 329 | nameCommands 330 | ); 331 | // run fly launch --no-deploy to allocate app 332 | 333 | await createkongYaml(); 334 | global.kong.ipv6 = await flyLaunchDeployInternalIPV6( 335 | kongLaunchCommandArray, 336 | global.kongPath 337 | ); 338 | await allocatePublicIPs(global.kongPath); 339 | kongSpinner.stop(); 340 | console.log(chalk.green("Kong deployed")); 341 | return; 342 | } 343 | //Deploying postgresT 344 | async function deployPGREST() { 345 | await updateFlyDBRoles(global.dbPath); 346 | let postgrestName; 347 | if (!global.yes) { 348 | postgrestName = await input({ 349 | message: 350 | "Enter a name for your postgREST instance, or leave blank for a generated one", 351 | }); 352 | } 353 | const pgRestSpinner = ora({ 354 | text: "Deploying postgREST", 355 | color: "yellow", 356 | }).start(); 357 | // if we dont have a name passed in, we need to generate one 358 | const nameCommands = postgrestName 359 | ? ["--name", postgrestName] 360 | : ["--generate-name"]; 361 | 362 | // create array of commands 363 | const pgLaunchCommandArray = ["launch"].concat( 364 | launchDefaultArgs, 365 | global.defaultArgs, 366 | nameCommands 367 | ); 368 | 369 | // create secrets 370 | const secrets = { 371 | PGRST_DB_URI: `postgres://authenticator:password@[${global.database.ipv6}]:5432/postgres`, 372 | PGRST_DB_ANON_ROLE: "anon", 373 | PGRST_DB_USE_LEGACY_GUCS: "false", 374 | PGRST_DB_SCHEMAS: "public,storage,graphql_public", 375 | PGRST_JWT_SECRET: global.jwtTokens.JWT_SECRET, 376 | }; 377 | 378 | // run fly launch --no-deploy to allocate app 379 | global.pgRest.ipv6 = await flyLaunchDeployInternalIPV6( 380 | pgLaunchCommandArray, 381 | global.pgRestPath, 382 | secrets 383 | ); 384 | await allocatePublicIPs(global.pgRestPath); 385 | global.pgRest.name = await getNameFromFlyStatus(global.pgRestPath); 386 | pgRestSpinner.stop(); 387 | console.log(chalk.green("PostgREST deployed")); 388 | return; 389 | } 390 | 391 | async function deployAuth() { 392 | let authName; 393 | if (!global.yes) { 394 | authName = await input({ 395 | message: 396 | "Enter a name for your auth instance, or leave blank for a generated one", 397 | }); 398 | } 399 | const authSpinner = ora({ 400 | text: "Deploying auth", 401 | color: "yellow", 402 | }).start(); 403 | // if we dont have a name passed in, we need to generate one 404 | const nameCommands = authName ? ["--name", authName] : ["--generate-name"]; 405 | 406 | // create array of commands 407 | const authLaunchCommandArray = ["launch"].concat( 408 | launchDefaultArgs, 409 | global.defaultArgs, 410 | nameCommands 411 | ); 412 | // run fly launch --no-deploy to allocate app 413 | global.pgAuth.ipv6 = await flyLaunchDeployInternalIPV6( 414 | authLaunchCommandArray, 415 | global.authPath 416 | ); 417 | const secrets = { 418 | PROJECT_ID: `supafly-${generate(1)}-${generate(1)}`, 419 | AUTH_EXTERNAL_GITHUB: "true", 420 | AUTH_SITE_URL: "https://example.com", 421 | GOTRUE_JWT_EXP: "86400", 422 | GOTRUE_API_PORT: 9999, 423 | GOTRUE_API_HOST: "fly-local-6pn", 424 | GOTRUE_DB_DRIVER: "postgres", 425 | GOTRUE_JWT_SECRET: global.jwtTokens.JWT_SECRET, 426 | GOTRUE_DISABLE_SIGNUP: "false", 427 | GOTRUE_EXTERNAL_EMAIL_ENABLED: "true", 428 | ENABLE_DOUBLE_CONFIRM: "false", 429 | GOTRUE_MAILER_AUTOCONFIRM: "false", 430 | GOTRUE_JWT_ADMIN_ROLES: "service_role", 431 | GOTRUE_JWT_AUD: "authenticated", 432 | GOTRUE_JWT_DEFAULT_GROUP_NAME: "authenticated", 433 | API_EXTERNAL_URL: "https://example.com", 434 | GOTRUE_SITE_URL: "https://example.com", 435 | GOTRUE_DB_DATABASE_URL: `postgres://supabase_auth_admin:password@${ 436 | "[" + global.database.ipv6 + "]" 437 | }:5432/postgres`, 438 | }; 439 | 440 | await setFlySecrets(secrets, global.authPath); 441 | authSpinner.stop(); 442 | console.log(chalk.green("Auth deployed")); 443 | return; 444 | } 445 | 446 | async function setFlySecrets(secrets: any, path: string) { 447 | const args = Object.entries(secrets).map(([key, value]) => `${key}=${value}`); 448 | 449 | const child = spawn("fly", ["secrets", "set", ...args], { cwd: path }); 450 | 451 | return await execAsync(child); 452 | } 453 | async function deployCleanUp() { 454 | if (!global.pgRest.ipv6) { 455 | global.pgRest.name = await getNameFromFlyStatus(global.pgRestPath); 456 | } 457 | if (!global.pgAuth.ipv6) { 458 | global.pgAuth.ipv6 = await getInternalIPV6Address(global.authPath); 459 | } 460 | if (!global.pgMeta.ipv6) { 461 | global.pgMeta.ipv6 = await getInternalIPV6Address(global.metaPath); 462 | } 463 | } 464 | async function deployDatabase() { 465 | // If they passed in yes, we need to generate a name 466 | if (!global.yes) { 467 | global.database.name = await input({ 468 | message: 469 | "Enter a name for your database, or leave blank for a generated one", 470 | }); 471 | } 472 | 473 | const dbSpinner = ora({ 474 | text: `Creating an application Fly.io's region ${global.defaultRegion} to host your database`, 475 | color: "blue", 476 | }).start(); 477 | 478 | // if we dont have a name passed in, we need to generate one 479 | const nameCommands = global.database.name 480 | ? ["--name", global.database.name] 481 | : ["--generate-name"]; 482 | 483 | // create array of commands 484 | const launchCommandArray = ["launch", "--internal-port", "5432"].concat( 485 | launchDefaultArgs, 486 | global.defaultArgs, 487 | nameCommands 488 | ); 489 | // i want to get the path of where stuff is being executed right now 490 | 491 | // run fly launch --no-deploy to allocate app 492 | const dbLaunch = spawn("fly", launchCommandArray, { 493 | cwd: global.dbPath, 494 | }); 495 | await execAsync(dbLaunch); 496 | 497 | dbSpinner.stop(); 498 | const ipv6Spinner = ora({ 499 | text: "Allocating private ipv6 address for your database", 500 | color: "yellow", 501 | }).start(); 502 | 503 | await allocatePrivateIPV6(global.dbPath); 504 | 505 | ipv6Spinner.stop(); 506 | const volumeSpinner = ora({ 507 | text: "Creating a volume for your database", 508 | color: "yellow", 509 | }).start(); 510 | 511 | await createFlyVolume(global.dbPath); 512 | 513 | volumeSpinner.stop(); 514 | const scaleSpinner = ora({ 515 | text: "Scaling your database to 1GB of memory and deploying to Fly.io 👟", 516 | color: "yellow", 517 | }).start(); 518 | 519 | await flyDeploy(global.dbPath, [ 520 | "--vm-memory", 521 | "1024", 522 | "--volume-initial-size", 523 | "3", 524 | ]); 525 | 526 | scaleSpinner.stop(); 527 | const dbStatusSpinner = ora({ 528 | text: "Waiting for your database to start", 529 | color: "yellow", 530 | }).start(); 531 | 532 | // wait 2 seconds for the database to start 533 | setTimeout(() => {}, 2500); 534 | setTimeout(() => {}, 2000); 535 | dbStatusSpinner.stop(); 536 | } 537 | 538 | async function createFlyVolume(path: string) { 539 | const command = "fly"; 540 | const args = [ 541 | "volumes", 542 | "create", 543 | "pg_data", 544 | "--region", 545 | global.defaultRegion, 546 | "--size", 547 | "3", 548 | "-n", 549 | "2", 550 | ]; 551 | 552 | const flyProcess = spawn(command, args, { 553 | cwd: path, 554 | }); 555 | await execAsync(flyProcess); 556 | } 557 | 558 | /** 559 | * @description Executes a child process and returns the response from stdout 560 | * @param spawn 561 | */ 562 | async function execAsync(spawn: ChildProcessWithoutNullStreams) { 563 | let response = ""; 564 | spawn.on("error", (err) => { 565 | console.log(`error: ${err.message}`); 566 | }); 567 | 568 | spawn.stderr.on("error", (data) => { 569 | console.log(`stderr: ${data}`); 570 | }); 571 | 572 | spawn.on("error", (err) => { 573 | console.error(`error message: ${err}`); 574 | throw err; // Throw the error to propagate it to the caller 575 | }); 576 | spawn.on("exit", (code, signal) => { 577 | if (code !== 0) { 578 | console.error( 579 | `child process exited with code ${code} and signal ${signal}` 580 | ); 581 | } 582 | }); 583 | 584 | for await (const data of spawn.stdout) { 585 | response += data.toString(); 586 | } 587 | return response; 588 | } 589 | async function execAsyncLog(spawn: ChildProcessWithoutNullStreams) { 590 | let response = ""; 591 | spawn.on("error", (err) => { 592 | console.log(`error: ${err.message}`); 593 | }); 594 | 595 | spawn.stderr.on("error", (data) => { 596 | console.log(`stderr: ${data}`); 597 | }); 598 | 599 | for await (const data of spawn.stdout) { 600 | response += data.toString(); 601 | } 602 | return response; 603 | } 604 | 605 | async function flyLaunchDeployInternalIPV6( 606 | launchCommandArray: string[], 607 | path: string, 608 | secrets?: any 609 | ) { 610 | // run fly launch --no-deploy to allocate app 611 | const launchCommand = spawn("fly", launchCommandArray, { 612 | cwd: path, 613 | }); 614 | await execAsync(launchCommand); 615 | await allocatePrivateIPV6(path); 616 | if (secrets) { 617 | await setFlySecrets(secrets, path); 618 | } 619 | await flyDeploy(path); 620 | setTimeout(() => {}, 2000); 621 | return await getInternalIPV6Address(path); 622 | } 623 | 624 | async function createDirectories() { 625 | if (!global.directory) { 626 | const directoryAction = await select({ 627 | message: "Save or delete Dockerfiles and fly configuration files?", 628 | choices: [ 629 | { 630 | name: "📁 Create a permanent directory for Dockerfile and fly.toml", 631 | value: "create", 632 | }, 633 | { 634 | name: chalk.red( 635 | "🗑️ Delete all Dockerfiles and fly configuration files after deployment" 636 | ), 637 | value: "delete", 638 | }, 639 | ], 640 | default: "create", 641 | }); 642 | if (directoryAction === "create") { 643 | global.directory = await input({ 644 | message: "Enter a name for your directory", 645 | default: "supafly", 646 | }); 647 | } else if (directoryAction === "delete") { 648 | global.directory = "./temp-supafly"; 649 | } 650 | 651 | // make directory with name global.directory 652 | const mkdir = spawn("mkdir", [global.directory]); 653 | await execAsync(mkdir); 654 | } 655 | } 656 | 657 | async function copyFilesToDirectory() { 658 | // mkdir for auth, database, kong, pg-rest, studio, and pg-meta 659 | const mkdirAuthPromise = execAsync( 660 | spawn("mkdir", [global.directory + "/auth"]) 661 | ); 662 | const mkdirDatabasePromise = execAsync( 663 | spawn("mkdir", [global.directory + "/database"]) 664 | ); 665 | const mkdirKongPromise = execAsync( 666 | spawn("mkdir", [global.directory + "/kong"]) 667 | ); 668 | const mkdirPgRestPromise = execAsync( 669 | spawn("mkdir", [global.directory + "/pg-rest"]) 670 | ); 671 | const mkdirStudioPromise = execAsync( 672 | spawn("mkdir", [global.directory + "/studio"]) 673 | ); 674 | const mkdirPgMetaPromise = execAsync( 675 | spawn("mkdir", [global.directory + "/pg-meta"]) 676 | ); 677 | await Promise.all([ 678 | mkdirAuthPromise, 679 | mkdirDatabasePromise, 680 | mkdirKongPromise, 681 | mkdirPgRestPromise, 682 | mkdirStudioPromise, 683 | mkdirPgMetaPromise, 684 | ]); 685 | 686 | // copy files to directory 687 | const cpAuthPromise = execAsync( 688 | spawn("cp", ["./src/auth/*", global.directory + "/auth"]) 689 | ); 690 | const cpDatabasePromise = execAsync( 691 | spawn("cp", ["./src/database/*", global.directory + "/database"]) 692 | ); 693 | const cpKongPromise = execAsync( 694 | spawn("cp", ["./src/kong/*", global.directory + "/kong"]) 695 | ); 696 | const cpPgRestPromise = execAsync( 697 | spawn("cp", ["./src/pg-rest/*", global.directory + "/pg-rest"]) 698 | ); 699 | const cpStudioPromise = execAsync( 700 | spawn("cp", ["./src/studio/*", global.directory + "/studio"]) 701 | ); 702 | 703 | await Promise.all([ 704 | cpAuthPromise, 705 | cpDatabasePromise, 706 | cpKongPromise, 707 | cpPgRestPromise, 708 | cpStudioPromise, 709 | ]); 710 | } 711 | 712 | async function flySetDefaultRegion() { 713 | // if no region is passed in as an option, we need to prompt them 714 | if (!global.defaultRegion) { 715 | let regionOptions = [ 716 | { 717 | city: "", 718 | code: "", 719 | }, 720 | ]; 721 | 722 | // Cal fly io to get list of regions 723 | const regionsSpawn = spawn("fly", ["platform", "regions"]); 724 | const regionChoices = (await execAsync(regionsSpawn)).split("\n").slice(1); 725 | for (let i = 0; i < regionChoices.length; i++) { 726 | const infoArray = regionChoices[i].split(`\t`); 727 | if (infoArray[1] && infoArray[0]) { 728 | regionOptions.push({ 729 | city: infoArray[0].trim(), 730 | code: infoArray[1].trim(), 731 | }); 732 | } 733 | } 734 | 735 | // filter out the empty values 736 | regionOptions = regionOptions.filter((o) => o.city !== ""); 737 | 738 | // prompt the user to select a region 739 | global.defaultRegion = await select({ 740 | message: "Select a default region", 741 | choices: regionOptions.map((o) => { 742 | return { 743 | name: o.city + " - " + o.code, 744 | value: o.code, 745 | }; 746 | }), 747 | }); 748 | } 749 | console.log("Deploying to region:", chalk.green(global.defaultRegion)); 750 | } 751 | 752 | async function flySetDefaultOrg() { 753 | // TODO: Prompt them with a list or orgs 754 | global.organization = options.org ?? "personal"; 755 | console.log("Deploying to organization:", chalk.green(global.organization)); 756 | } 757 | 758 | async function flyDeployAndPrepareDB() { 759 | if (!options.dbUrl) { 760 | // deploy database 761 | await deployDatabase(); 762 | const dbStatusSpinner = ora({ 763 | text: "getting database ipv6 address", 764 | color: "yellow", 765 | }).start(); 766 | global.database.ipv6 = await getInternalIPV6Address(global.dbPath); 767 | dbStatusSpinner.stop(); 768 | console.log(chalk.green("You successfully deployed your database!")); 769 | } 770 | } 771 | 772 | async function allocatePublicIPs(path: string) { 773 | const ips4 = spawn("fly", ["ips", "allocate-v4", "--shared"], { 774 | cwd: path, 775 | }); 776 | const ips6 = spawn("fly", ["ips", "allocate-v6"], { 777 | cwd: path, 778 | }); 779 | await execAsync(ips6); 780 | return await execAsync(ips4); 781 | } 782 | async function allocatePrivateIPV6(path: string) { 783 | const ips = spawn("fly", ["ips", "allocate-v6", "--private"], { 784 | cwd: path, 785 | }); 786 | 787 | return await execAsync(ips); 788 | } 789 | 790 | async function getNameFromFlyStatus(path: string) { 791 | const flyStatus = spawn("fly", ["status"], { 792 | cwd: path, 793 | }); 794 | const result = await execAsync(flyStatus); 795 | const regex = /Name\s+=\s+(\S+)/; 796 | const res = result.match(regex); 797 | if (res) { 798 | return res[1]; 799 | } else { 800 | console.error("Name not found: ", path); 801 | console.error(result); 802 | } 803 | } 804 | 805 | async function flyDeploy(path: string, args: string[] = []) { 806 | const commands = ["deploy"].concat(args); 807 | const flyDeploy = spawn("fly", commands, { 808 | cwd: path, 809 | }); 810 | return await execAsync(flyDeploy); 811 | } 812 | 813 | async function getInternalIPV6Address(projPath: string) { 814 | const copyHostFile = spawn( 815 | "fly", 816 | ["ssh", "console", "--command", "cat etc/hosts"], 817 | { 818 | cwd: projPath, 819 | } 820 | ); 821 | const result = await execAsync(copyHostFile); 822 | // Extract the IPv6 address before "fly-local-6pn" 823 | const match = result.match(/([0-9a-fA-F:]+)\s+fly-local-6pn/); 824 | let ipv6 = ""; 825 | if (match) { 826 | ipv6 = match[1]; 827 | } 828 | return ipv6; 829 | } 830 | 831 | async function updatePGMetaDockerFilePGHost( 832 | filePath: string, 833 | newInternalAddress: string 834 | ) { 835 | try { 836 | const data = await readFile(filePath, "utf8"); 837 | 838 | const regex = /PG_META_DB_HOST=".*"/g; 839 | const newContent = data.replace( 840 | regex, 841 | `PG_META_DB_HOST="[${newInternalAddress}]"` 842 | ); 843 | 844 | await writeFile(filePath, newContent, "utf8"); 845 | } catch (err) { 846 | console.error(err); 847 | } 848 | } 849 | 850 | async function apiGatewayTest() { 851 | global.kong.publicUrl = (await getNameFromFlyStatus(global.kongPath)) ?? ""; 852 | const link = `https://${global.kong.publicUrl}.fly.dev/test`; 853 | console.log( 854 | "Click this link to test your Supabase deployment:", 855 | chalk.green(link) 856 | ); 857 | } 858 | async function studioTest() { 859 | global.studio.publicUrl = 860 | (await getNameFromFlyStatus(global.studioPath)) ?? ""; 861 | const studioLink = `https://${global.studio.publicUrl}.fly.dev`; 862 | console.log( 863 | "Click this link to visit your Supabase studio:", 864 | chalk.green(studioLink) 865 | ); 866 | } 867 | 868 | function generateSupaJWTs() { 869 | var signingKey = secureRandom(256, { type: "Buffer" }); 870 | const anonClaims = { 871 | role: "anon", 872 | iss: "supabase", 873 | }; 874 | const serviceClaims = { 875 | role: "service_role", 876 | iss: "supabase", 877 | }; 878 | 879 | global.jwtTokens.anonToken = njwt.create(anonClaims, signingKey).compact(); 880 | global.jwtTokens.serviceToken = njwt 881 | .create(serviceClaims, signingKey) 882 | .compact(); 883 | global.jwtTokens.JWT_SECRET = signingKey.toString("hex"); 884 | 885 | return; 886 | } 887 | 888 | async function createkongYaml() { 889 | const kongYaml = `_format_version: '1.1' 890 | 891 | ### 892 | ### Consumers / Users 893 | ### 894 | consumers: 895 | - username: anon 896 | keyauth_credentials: 897 | - key: ${global.jwtTokens.anonToken} 898 | - username: service_role 899 | keyauth_credentials: 900 | - key: ${global.jwtTokens.serviceToken} 901 | 902 | ### 903 | ### Access Control List 904 | ### 905 | acls: 906 | - consumer: anon 907 | group: anon 908 | - consumer: service_role 909 | group: admin 910 | 911 | ### 912 | ### API Routes 913 | ### 914 | services: 915 | ## Open Auth routes 916 | - name: test 917 | url: https://kongtest.nick-prim.workers.dev/ 918 | routes: 919 | - name: test 920 | strip_path: true 921 | paths: 922 | - /test 923 | plugins: 924 | - name: cors 925 | - name: auth-v1-open 926 | host: "[${global.pgAuth.ipv6}]" 927 | port: 9999 928 | routes: 929 | - name: auth-v1-open 930 | strip_path: true 931 | paths: 932 | - /auth/v1/verify 933 | plugins: 934 | - name: cors 935 | - name: auth-v1-open-callback 936 | host: "[${global.pgAuth.ipv6}]" 937 | port: 9999 938 | routes: 939 | - name: auth-v1-open-callback 940 | strip_path: true 941 | paths: 942 | - /auth/v1/callback 943 | plugins: 944 | - name: cors 945 | - name: auth-v1-open-authorize 946 | host: "[${global.pgAuth.ipv6}]" 947 | port: 9999 948 | routes: 949 | - name: auth-v1-open-authorize 950 | strip_path: true 951 | paths: 952 | - /auth/v1/authorize 953 | plugins: 954 | - name: cors 955 | 956 | ## Secure Auth routes 957 | - name: auth-v1 958 | host: "[${global.pgAuth.ipv6}]" 959 | port: 9999 960 | routes: 961 | - name: auth-v1-all 962 | strip_path: true 963 | paths: 964 | - /auth/v1/ 965 | plugins: 966 | - name: cors 967 | - name: key-auth 968 | config: 969 | hide_credentials: false 970 | - name: acl 971 | config: 972 | hide_groups_header: true 973 | allow: 974 | - admin 975 | - anon 976 | 977 | ## Secure REST routes 978 | - name: rest-v1 979 | url: "https://${global.pgRest.name}.fly.dev/" 980 | routes: 981 | - name: rest-v1-all 982 | strip_path: true 983 | paths: 984 | - /rest/v1/ 985 | plugins: 986 | - name: cors 987 | - name: key-auth 988 | config: 989 | hide_credentials: true 990 | - name: acl 991 | config: 992 | hide_groups_header: true 993 | allow: 994 | - admin 995 | - anon 996 | ## Secure Database routes 997 | - name: meta 998 | host: "[${global.pgMeta.ipv6}]" 999 | port: 8080 1000 | routes: 1001 | - name: meta-all 1002 | strip_path: true 1003 | paths: 1004 | - /pg/ 1005 | 1006 | `; 1007 | const KONG_YML_PATH = global.kongPath + "/kong.yml"; 1008 | await writeFile(KONG_YML_PATH, kongYaml, "utf8"); 1009 | return; 1010 | } 1011 | 1012 | const launchDefaultArgs = [ 1013 | "--no-deploy", 1014 | "--copy-config", 1015 | "--reuse-app", 1016 | "--legacy", 1017 | "--force-machines", 1018 | ]; 1019 | 1020 | type GlobalInfo = { 1021 | username: string; 1022 | defaultRegion: string; 1023 | organization: string; 1024 | pgMeta: serviceInfo; 1025 | pgRest: serviceInfo; 1026 | jwtTokens: { 1027 | anonToken: string; 1028 | serviceToken: string; 1029 | JWT_SECRET?: string; 1030 | }; 1031 | pgAuth: serviceInfo; 1032 | database: serviceInfo & { 1033 | hostname?: string; 1034 | port?: string; 1035 | username?: string; 1036 | password?: string; 1037 | databaseName?: string; 1038 | }; 1039 | studio: { 1040 | ipv6: string; 1041 | publicUrl: string; 1042 | }; 1043 | kong: { 1044 | ipv6: string; 1045 | publicUrl: string; 1046 | }; 1047 | defaultArgs: string[]; 1048 | dbPath: string; 1049 | directory: string; 1050 | pgRestPath: string; 1051 | authPath: string; 1052 | studioPath: string; 1053 | kongPath: string; 1054 | metaPath: string; 1055 | yes?: boolean; 1056 | FLY_API_TOKEN: string; 1057 | }; 1058 | 1059 | type serviceInfo = { 1060 | name?: string; 1061 | ipv6: string; 1062 | }; 1063 | 1064 | type cliInput = { 1065 | dir?: string; 1066 | org?: string; 1067 | region?: string; 1068 | dbUrl?: string; 1069 | yes?: boolean; 1070 | }; 1071 | -------------------------------------------------------------------------------- /src/kong/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kong 2 | 3 | COPY kong.yml /var/lib/kong/kong.yml 4 | ENV KONG_DNS_ORDER=LAST,A,CNAME 5 | ENV KONG_PLUGINS=request-transformer,cors,key-auth,acl 6 | ENV KONG_NGINX_PROXY_PROXY_BUFFER_SIZE=160k 7 | ENV KONG_NGINX_PROXY_PROXY_BUFFERS="64 160k" 8 | ENV KONG_DATABASE=off 9 | ENV KONG_DECLARATIVE_CONFIG=/var/lib/kong/kong.yml 10 | EXPOSE 8000 8443 8001 8444 8000/tcp 8443/tcp 8002 11 | 12 | -------------------------------------------------------------------------------- /src/kong/kong.yml: -------------------------------------------------------------------------------- 1 | _format_version: '1.1' 2 | 3 | ### 4 | ### Consumers / Users 5 | ### 6 | consumers: 7 | - username: anon 8 | keyauth_credentials: 9 | - key: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwianRpIjoiMjVhYTQ5YTYtZTdjMi00ZDFlLTkyNWYtMWY5MjMxOTYxMjFkIiwiaWF0IjoxNjgzNjM4Mjg0LCJleHAiOjE2ODM2NDE4ODR9.3caVrpdo8l7l3uSCChe3JInrTln4evTGwb6LAI_SdnU 10 | - username: service_role 11 | keyauth_credentials: 12 | - key: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJqdGkiOiJhZTNlNWFmZi02ODNlLTRjMGItYTQ5MC1iYzMxZTU0NDhkZjUiLCJpYXQiOjE2ODM2MzgyODQsImV4cCI6MTY4MzY0MTg4NH0.Xf-Z7g5YwMbg04NfvTdtp5GhcqCXrQdPltN_47uMFfw 13 | 14 | ### 15 | ### Access Control List 16 | ### 17 | acls: 18 | - consumer: anon 19 | group: anon 20 | - consumer: service_role 21 | group: admin 22 | 23 | ### 24 | ### API Routes 25 | ### 26 | services: 27 | ## Open Auth routes 28 | - name: test 29 | url: https://kongtest.nick-prim.workers.dev/ 30 | routes: 31 | - name: test 32 | strip_path: true 33 | paths: 34 | - /test 35 | plugins: 36 | - name: cors 37 | - name: auth-v1-open 38 | host: "[fdaa:2:21ef:a7b:104:294c:d066:2]" 39 | port: 9999 40 | routes: 41 | - name: auth-v1-open 42 | strip_path: true 43 | paths: 44 | - /auth/v1/verify 45 | plugins: 46 | - name: cors 47 | - name: auth-v1-open-callback 48 | host: "[fdaa:2:21ef:a7b:104:294c:d066:2]" 49 | port: 9999 50 | routes: 51 | - name: auth-v1-open-callback 52 | strip_path: true 53 | paths: 54 | - /auth/v1/callback 55 | plugins: 56 | - name: cors 57 | - name: auth-v1-open-authorize 58 | host: "[fdaa:2:21ef:a7b:104:294c:d066:2]" 59 | port: 9999 60 | routes: 61 | - name: auth-v1-open-authorize 62 | strip_path: true 63 | paths: 64 | - /auth/v1/authorize 65 | plugins: 66 | - name: cors 67 | 68 | ## Secure Auth routes 69 | - name: auth-v1 70 | host: "[fdaa:2:21ef:a7b:104:294c:d066:2]" 71 | port: 9999 72 | routes: 73 | - name: auth-v1-all 74 | strip_path: true 75 | paths: 76 | - /auth/v1/ 77 | plugins: 78 | - name: cors 79 | - name: key-auth 80 | config: 81 | hide_credentials: false 82 | - name: acl 83 | config: 84 | hide_groups_header: true 85 | allow: 86 | - admin 87 | - anon 88 | 89 | ## Secure REST routes 90 | - name: rest-v1 91 | url: "https://quiet-snow-8383.fly.dev/" 92 | routes: 93 | - name: rest-v1-all 94 | strip_path: true 95 | paths: 96 | - /rest/v1/ 97 | plugins: 98 | - name: cors 99 | - name: key-auth 100 | config: 101 | hide_credentials: true 102 | - name: acl 103 | config: 104 | hide_groups_header: true 105 | allow: 106 | - admin 107 | - anon 108 | ## Secure Database routes 109 | - name: meta 110 | host: "[fdaa:2:21ef:a7b:f8:e57d:369d:2]" 111 | port: 8080 112 | routes: 113 | - name: meta-all 114 | strip_path: true 115 | paths: 116 | - /pg/ 117 | 118 | -------------------------------------------------------------------------------- /src/pg-rest/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgrest/postgrest 2 | 3 | -------------------------------------------------------------------------------- /src/studio/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM supabase/studio:20230330-99fed3d 2 | RUN rm -rf /usr/src/app/.env 3 | -------------------------------------------------------------------------------- /supabased.conf: -------------------------------------------------------------------------------- 1 | 2 | [Interface] 3 | PrivateKey = zSWzfer/6IZB8LhDgSbha1UqYbAH3JU+R0cveNUweIQ= 4 | Address = fdaa:2:21ef:a7b:1537:0:a:102/120 5 | DNS = fdaa:2:21ef::3 6 | 7 | [Peer] 8 | PublicKey = vsNMvNdCmUiVzwp0v6bkeO0pcMhy4pak2E2Z/CF2ywA= 9 | AllowedIPs = fdaa:2:21ef::/48 10 | Endpoint = lax1.gateway.6pn.dev:51820 11 | PersistentKeepalive = 15 12 | 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "skipLibCheck": true, 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "strict": true, 7 | "target": "ESNext", 8 | "module": "NodeNext", 9 | "sourceMap": true, 10 | "esModuleInterop": true 11 | }, 12 | "include": ["src/**/*"] 13 | } 14 | --------------------------------------------------------------------------------