├── .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 |
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 |
--------------------------------------------------------------------------------