From adb55f72d1bcab494d47482a83169771b7b5e555 Mon Sep 17 00:00:00 2001 From: Gleb Khaykin Date: Fri, 20 Dec 2024 16:36:34 +0700 Subject: [PATCH 01/20] build(deps): use stable react 19 --- package.json | 4 +-- pnpm-lock.yaml | 94 +++++++++++++++++++++++++------------------------- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 79c1503..61de892 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "katex": "^0.16.11", "notion-client": "^6.16.0", "prismjs": "^1.29.0", - "react": "19.0.0-rc-cd22717c-20241013", - "react-dom": "19.0.0-rc-cd22717c-20241013", + "react": "19.0.0", + "react-dom": "19.0.0", "react-icons": "^5.3.0", "react-notion-x": "link:vendor/react-notion-x/packages/react-notion-x", "remix-utils": "^7.7.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb753ce..10cbf09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,13 +10,13 @@ importers: dependencies: '@nasa-gcn/remix-seo': specifier: ^2.0.1 - version: 2.0.1(@remix-run/react@2.11.2(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)(typescript@5.5.4))(@remix-run/server-runtime@2.11.2(typescript@5.5.4)) + version: 2.0.1(@remix-run/react@2.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.5.4))(@remix-run/server-runtime@2.11.2(typescript@5.5.4)) '@remix-run/node': specifier: ^2.11.2 version: 2.11.2(typescript@5.5.4) '@remix-run/react': specifier: ^2.11.2 - version: 2.11.2(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)(typescript@5.5.4) + version: 2.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.5.4) '@remix-run/serve': specifier: ^2.11.2 version: 2.11.2(typescript@5.5.4) @@ -25,10 +25,10 @@ importers: version: 2.46.1 '@vercel/analytics': specifier: ^1.3.1 - version: 1.3.1(react@19.0.0-rc-cd22717c-20241013) + version: 1.3.1(react@19.0.0) '@vercel/speed-insights': specifier: ^1.0.12 - version: 1.0.12(react@19.0.0-rc-cd22717c-20241013) + version: 1.0.12(react@19.0.0) isbot: specifier: ^4.4.0 version: 4.4.0 @@ -42,24 +42,24 @@ importers: specifier: ^1.29.0 version: 1.29.0 react: - specifier: 19.0.0-rc-cd22717c-20241013 - version: 19.0.0-rc-cd22717c-20241013 + specifier: 19.0.0 + version: 19.0.0 react-dom: - specifier: 19.0.0-rc-cd22717c-20241013 - version: 19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013) + specifier: 19.0.0 + version: 19.0.0(react@19.0.0) react-icons: specifier: ^5.3.0 - version: 5.3.0(react@19.0.0-rc-cd22717c-20241013) + version: 5.3.0(react@19.0.0) react-notion-x: specifier: link:vendor/react-notion-x/packages/react-notion-x version: link:vendor/react-notion-x/packages/react-notion-x remix-utils: specifier: ^7.7.0 - version: 7.7.0(@remix-run/node@2.11.2(typescript@5.5.4))(@remix-run/react@2.11.2(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)(typescript@5.5.4))(@remix-run/router@1.19.1)(react@19.0.0-rc-cd22717c-20241013) + version: 7.7.0(@remix-run/node@2.11.2(typescript@5.5.4))(@remix-run/react@2.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.5.4))(@remix-run/router@1.19.1)(react@19.0.0) devDependencies: '@remix-run/dev': specifier: ^2.11.2 - version: 2.11.2(@remix-run/react@2.11.2(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)(typescript@5.5.4))(@remix-run/serve@2.11.2(typescript@5.5.4))(@types/node@22.5.4)(typescript@5.5.4)(vite@5.4.3(@types/node@22.5.4)) + version: 2.11.2(@remix-run/react@2.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.5.4))(@remix-run/serve@2.11.2(typescript@5.5.4))(@types/node@22.5.4)(typescript@5.5.4)(vite@5.4.3(@types/node@22.5.4)) '@types/prismjs': specifier: ^1.26.4 version: 1.26.4 @@ -3345,10 +3345,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} - react-dom@19.0.0-rc-cd22717c-20241013: - resolution: {integrity: sha512-NzjTBOXygonUxLRQuUUW5V2QLGkAcyUwJoS8+UWxs089paMvQQfoRD51w65Ovgd2OEQ8Rm3HWx+82fvXiT0czQ==} + react-dom@19.0.0: + resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} peerDependencies: - react: 19.0.0-rc-cd22717c-20241013 + react: ^19.0.0 react-icons@5.3.0: resolution: {integrity: sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==} @@ -3375,8 +3375,8 @@ packages: peerDependencies: react: '>=16.8' - react@19.0.0-rc-cd22717c-20241013: - resolution: {integrity: sha512-k28GszmyQ1tX/JmeLGZINq5KXiNy/MmN0fCAtcwF8a9INDyVYG0zATCRGJwaPB9WixmkuwPv1BfB1QBfJC7cNg==} + react@19.0.0: + resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -3535,8 +3535,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - scheduler@0.25.0-rc-cd22717c-20241013: - resolution: {integrity: sha512-MnsFR57bKcrYslnbCUsaUG0qBuAArk92VxE0zu6A2Usz38iIuL2uZLunqKlP1W47MF33GrRGDj1sXdPbFKIZfw==} + scheduler@0.25.0: + resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} @@ -4614,9 +4614,9 @@ snapshots: '@microsoft/tsdoc@0.15.0': {} - '@nasa-gcn/remix-seo@2.0.1(@remix-run/react@2.11.2(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)(typescript@5.5.4))(@remix-run/server-runtime@2.11.2(typescript@5.5.4))': + '@nasa-gcn/remix-seo@2.0.1(@remix-run/react@2.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.5.4))(@remix-run/server-runtime@2.11.2(typescript@5.5.4))': dependencies: - '@remix-run/react': 2.11.2(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)(typescript@5.5.4) + '@remix-run/react': 2.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.5.4) '@remix-run/server-runtime': 2.11.2(typescript@5.5.4) lodash: 4.17.21 @@ -4672,7 +4672,7 @@ snapshots: '@pkgr/core@0.1.1': {} - '@remix-run/dev@2.11.2(@remix-run/react@2.11.2(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)(typescript@5.5.4))(@remix-run/serve@2.11.2(typescript@5.5.4))(@types/node@22.5.4)(typescript@5.5.4)(vite@5.4.3(@types/node@22.5.4))': + '@remix-run/dev@2.11.2(@remix-run/react@2.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.5.4))(@remix-run/serve@2.11.2(typescript@5.5.4))(@types/node@22.5.4)(typescript@5.5.4)(vite@5.4.3(@types/node@22.5.4))': dependencies: '@babel/core': 7.25.2 '@babel/generator': 7.25.6 @@ -4685,7 +4685,7 @@ snapshots: '@mdx-js/mdx': 2.3.0 '@npmcli/package-json': 4.0.1 '@remix-run/node': 2.11.2(typescript@5.5.4) - '@remix-run/react': 2.11.2(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)(typescript@5.5.4) + '@remix-run/react': 2.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.5.4) '@remix-run/router': 1.19.1 '@remix-run/server-runtime': 2.11.2(typescript@5.5.4) '@types/mdx': 2.0.13 @@ -4767,14 +4767,14 @@ snapshots: optionalDependencies: typescript: 5.5.4 - '@remix-run/react@2.11.2(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)(typescript@5.5.4)': + '@remix-run/react@2.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.5.4)': dependencies: '@remix-run/router': 1.19.1 '@remix-run/server-runtime': 2.11.2(typescript@5.5.4) - react: 19.0.0-rc-cd22717c-20241013 - react-dom: 19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013) - react-router: 6.26.1(react@19.0.0-rc-cd22717c-20241013) - react-router-dom: 6.26.1(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-router: 6.26.1(react@19.0.0) + react-router-dom: 6.26.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) turbo-stream: 2.3.0 optionalDependencies: typescript: 5.5.4 @@ -5149,15 +5149,15 @@ snapshots: '@vanilla-extract/private@1.0.6': {} - '@vercel/analytics@1.3.1(react@19.0.0-rc-cd22717c-20241013)': + '@vercel/analytics@1.3.1(react@19.0.0)': dependencies: server-only: 0.0.1 optionalDependencies: - react: 19.0.0-rc-cd22717c-20241013 + react: 19.0.0 - '@vercel/speed-insights@1.0.12(react@19.0.0-rc-cd22717c-20241013)': + '@vercel/speed-insights@1.0.12(react@19.0.0)': optionalDependencies: - react: 19.0.0-rc-cd22717c-20241013 + react: 19.0.0 '@web3-storage/multipart-parser@1.0.0': {} @@ -7754,32 +7754,32 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013): + react-dom@19.0.0(react@19.0.0): dependencies: - react: 19.0.0-rc-cd22717c-20241013 - scheduler: 0.25.0-rc-cd22717c-20241013 + react: 19.0.0 + scheduler: 0.25.0 - react-icons@5.3.0(react@19.0.0-rc-cd22717c-20241013): + react-icons@5.3.0(react@19.0.0): dependencies: - react: 19.0.0-rc-cd22717c-20241013 + react: 19.0.0 react-is@16.13.1: {} react-refresh@0.14.2: {} - react-router-dom@6.26.1(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013): + react-router-dom@6.26.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@remix-run/router': 1.19.1 - react: 19.0.0-rc-cd22717c-20241013 - react-dom: 19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013) - react-router: 6.26.1(react@19.0.0-rc-cd22717c-20241013) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-router: 6.26.1(react@19.0.0) - react-router@6.26.1(react@19.0.0-rc-cd22717c-20241013): + react-router@6.26.1(react@19.0.0): dependencies: '@remix-run/router': 1.19.1 - react: 19.0.0-rc-cd22717c-20241013 + react: 19.0.0 - react@19.0.0-rc-cd22717c-20241013: {} + react@19.0.0: {} read-cache@1.0.0: dependencies: @@ -7860,14 +7860,14 @@ snapshots: mdast-util-to-hast: 12.3.0 unified: 10.1.2 - remix-utils@7.7.0(@remix-run/node@2.11.2(typescript@5.5.4))(@remix-run/react@2.11.2(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)(typescript@5.5.4))(@remix-run/router@1.19.1)(react@19.0.0-rc-cd22717c-20241013): + remix-utils@7.7.0(@remix-run/node@2.11.2(typescript@5.5.4))(@remix-run/react@2.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.5.4))(@remix-run/router@1.19.1)(react@19.0.0): dependencies: type-fest: 4.26.1 optionalDependencies: '@remix-run/node': 2.11.2(typescript@5.5.4) - '@remix-run/react': 2.11.2(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)(typescript@5.5.4) + '@remix-run/react': 2.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.5.4) '@remix-run/router': 1.19.1 - react: 19.0.0-rc-cd22717c-20241013 + react: 19.0.0 require-from-string@2.0.2: {} @@ -7961,7 +7961,7 @@ snapshots: safer-buffer@2.1.2: {} - scheduler@0.25.0-rc-cd22717c-20241013: {} + scheduler@0.25.0: {} semver@6.3.1: {} From d796f7e4a35fe09606e17705852d36b91f563653 Mon Sep 17 00:00:00 2001 From: Gleb Khaykin Date: Fri, 20 Dec 2024 17:08:31 +0700 Subject: [PATCH 02/20] chore: add dotenv --- .envrc | 8 ++++ .tool-versions | 3 +- Makefile | 104 ++++++++++++++++++++++++++++++++++++------------- 3 files changed, 87 insertions(+), 28 deletions(-) create mode 100644 .envrc diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..9d21564 --- /dev/null +++ b/.envrc @@ -0,0 +1,8 @@ +# This file is used by direnv to automatically load environment variables +# from the .env file whenever you enter this directory. + +# To ensure security, direnv requires you to explicitly allow the execution +# of this file by running 'direnv allow' after any changes. +# This prevents unauthorized scripts from being executed automatically. + +dotenv diff --git a/.tool-versions b/.tool-versions index 1210af5..bb21c13 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,5 +1,6 @@ +direnv 2.35.0 +pre-commit 3.7.0 nodejs 21.7.3 pnpm 9.9.0 yarn 1.22.22 terraform 1.5.4 -pre-commit 3.7.0 diff --git a/Makefile b/Makefile index 7f5aed5..ea2b3a1 100644 --- a/Makefile +++ b/Makefile @@ -1,37 +1,58 @@ -SHELL := /bin/bash .DEFAULT_GOAL = help +SHELL := /bin/bash +VENDOR_DIR := vendor/react-notion-x + +# https://supabase.com/docs/guides/local-development/overview#database-migrations +# https://supabase.com/docs/guides/local-development/seeding-your-database +# https://supabase.com/docs/guides/self-hosting/docker +# https://supabase.com/docs/reference/javascript/typescript-support?queryGroups=platform&platform=pnpm + +# Load environment variables from .env +ifneq (,$(wildcard ./.env)) + include .env + export +endif +##============================================================================= ##@ Repo Initialization +##============================================================================= -prerequisite: ## Install prerequisite tools +PLUGINS := \ + direnv https://github.com/asdf-community/asdf-direnv.git \ + pre-commit https://github.com/jonathanmorley/asdf-pre-commit.git \ + nodejs https://github.com/asdf-vm/asdf-nodejs.git \ + pnpm https://github.com/jonathanmorley/asdf-pnpm.git \ + yarn https://github.com/twuni/asdf-yarn.git \ + terraform https://github.com/asdf-community/asdf-hashicorp.git + +prerequisites: ## Install prerequisite tools @echo "Checking and installing required plugins." - @plugins=( \ - "terraform https://github.com/asdf-community/asdf-hashicorp.git" \ - "pnpm https://github.com/jonathanmorley/asdf-pnpm.git" \ - "yarn https://github.com/twuni/asdf-yarn.git" \ - "nodejs https://github.com/asdf-vm/asdf-nodejs.git" \ - "pre-commit https://github.com/jonathanmorley/asdf-pre-commit.git" \ - ); \ - for info in "$${plugins[@]}"; do \ - read plugin url <<< "$$info"; \ + @echo "$(PLUGINS)" | tr ' ' '\n' | paste - - | while read -r plugin url; do \ if asdf plugin-list | grep -q $$plugin; then \ echo "Plugin '$$plugin' is already installed."; \ else \ echo "Adding plugin '$$plugin'."; \ asdf plugin-add $$plugin $$url; \ fi; \ - done; + done @echo "Installing specified versions." asdf install @echo "Currently installed versions:" asdf current -.PHONY: prerequisite +.PHONY: prerequisites + +env: ## Create .env file if it doesn't exist + @if ! [ -e .env ]; then \ + cp .env.example .env; \ + echo "Created .env file. Please edit it according to your setup."; \ + fi +.PHONY: env deps-notion: ## Install dependencies for react-notion-x @echo "Installing dependencies for react-notion-x." git submodule update --init --recursive - cd vendor/react-notion-x && yarn install --frozen-lockfile -.PHONY: notion-deps + cd $(VENDOR_DIR) && yarn install --frozen-lockfile +.PHONY: deps-notion deps: deps-notion ## Install repo dependencies @echo "Installing dependencies." @@ -46,15 +67,18 @@ deps-prod: deps-notion ## Install production dependencies pre-commit: ## Install pre-commit hooks @echo "Installing pre-commit hooks." pre-commit install -t pre-commit -t commit-msg +.PHONY: pre-commit -init: prerequisite deps pre-commit ## Initialize local environment for development +init: prerequisites env deps pre-commit ## Initialize local environment for development .PHONY: init +##============================================================================= ##@ Scripts +##============================================================================= build-notion: ## Build react-notion-x @echo "Building react-notion-x." - cd vendor/react-notion-x && yarn build + cd $(VENDOR_DIR) && yarn build .PHONY: build-notion build: build-notion ## Build project @@ -89,33 +113,59 @@ ngrok-dev: ## Run ngrok for development server ngrok-prod: ## Run ngrok for production server @echo "Running ngrok." - ngrok http 3000 + ngrok http 55203 .PHONY: ngrok-prod -##@ Miscullaneous - -create-secrets-baseline: ## Create secrets baseline file +##============================================================================= +##@ Supabase +##============================================================================= + +supabase-run: ## Run supabase stack + @echo "Running supabase." + docker compose up -d + @echo "PG Connection URI: ${POSTGRES_URI}" + @echo + @echo "JWT Secret: ${JWT_SECRET}" + @echo "Anon Key: ${ANON_KEY}" + @echo "Service Key: ${SERVICE_ROLE_KEY}" + @echo + @echo "API Server: http://localhost:${KONG_HTTP_PORT}/rest/v1/" + @echo "Supabase Studio (Dashboard): http://localhost:${STUDIO_PORT}" +.PHONY: supabase-run + +supabase-stop: ## Stop supabase stack + @echo "Stopping supabase." + docker compose down +.PHONY: supabase-stop + +##============================================================================= +##@ Miscellaneous +##============================================================================= + +create-secrets-baseline: ## Create secrets baseline file detect-secrets scan > .secrets.baseline .PHONY: create-secrets-baseline -audit-secrets-baseline: ## Check updated .secrets.baseline file +audit-secrets-baseline: ## Check updated .secrets.baseline file detect-secrets audit .secrets.baseline git commit .secrets.baseline --no-verify -m "build(security): update secrets.baseline" .PHONY: audit-secrets-baseline -update-pre-commit-hooks: ## Update pre-commit hooks +update-pre-commit-hooks: ## Update pre-commit hooks pre-commit autoupdate .PHONY: update-pre-commit-hooks clean: ## Clean project @echo "Cleaning project." rm -rf node_modules build - find vendor/react-notion-x -type d -name 'build' -exec rm -rf {} + - find vendor/react-notion-x -type d -name 'node_modules' -exec rm -rf {} + + find $(VENDOR_DIR) -type d -name 'build' -exec rm -rf {} + + find $(VENDOR_DIR) -type d -name 'node_modules' -exec rm -rf {} + .PHONY: clean +##============================================================================= ##@ Helper +##============================================================================= -help: ## Display help - @awk 'BEGIN {FS = ":.*##"; printf "\nUsage: \033[36m\033[0m\n"} /^[a-zA-Z\.\%-]+:.*?##/ { printf " \033[36m%-24s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) +help: ## Display help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-24s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) .PHONY: help From 9acc9f7acf5be488f7ab58493446bcf8c962d5c6 Mon Sep 17 00:00:00 2001 From: Gleb Khaykin Date: Tue, 24 Dec 2024 14:05:50 +0700 Subject: [PATCH 03/20] ci(hooks): ignore vendor and volumes for linter and formatter --- .pre-commit-config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9220bfa..6b0795d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,6 +44,7 @@ repos: types: [file] files: \.(js|ts|jsx|tsx)$ args: ["--quiet"] + exclude: ^(vendor/|volumes/) - repo: local hooks: @@ -53,6 +54,7 @@ repos: language: system types: [file] files: \.(js|ts|jsx|tsx|css|md|json|yaml)$ + exclude: ^(vendor/|volumes/) - repo: https://github.com/streetsidesoftware/cspell-cli rev: v8.13.3 From 6fc57ee28a181d556b16e08b6577109682363dc9 Mon Sep 17 00:00:00 2001 From: Gleb Khaykin Date: Tue, 24 Dec 2024 14:06:13 +0700 Subject: [PATCH 04/20] chore: update gitignore --- .gitignore | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 855c2c3..6f866eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,16 @@ +# Node node_modules -/.cache /build -.env +# Terraform terraform.tfstate terraform.tfstate.backup terraform.tfvars .terraform +# Supabase +volumes/db/data + +# Misc +.env .DS_Store From 670650f0afbf3471fc39f0ee83feb71ecc4b8681 Mon Sep 17 00:00:00 2001 From: Gleb Khaykin Date: Tue, 24 Dec 2024 14:14:22 +0700 Subject: [PATCH 05/20] chore: update gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6f866eb..a063438 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ terraform.tfvars .terraform # Supabase -volumes/db/data +supabase/volumes/db/data # Misc .env From 9fe7ebb4ba6b838cee27ec1c6dead77e8a07d829 Mon Sep 17 00:00:00 2001 From: Gleb Khaykin Date: Tue, 24 Dec 2024 14:15:47 +0700 Subject: [PATCH 06/20] ci(hooks): ignore vendor and supabase for linter and formatter --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6b0795d..f146c47 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: types: [file] files: \.(js|ts|jsx|tsx)$ args: ["--quiet"] - exclude: ^(vendor/|volumes/) + exclude: ^(vendor/|supabase/) - repo: local hooks: @@ -54,7 +54,7 @@ repos: language: system types: [file] files: \.(js|ts|jsx|tsx|css|md|json|yaml)$ - exclude: ^(vendor/|volumes/) + exclude: ^(vendor/|supabase/) - repo: https://github.com/streetsidesoftware/cspell-cli rev: v8.13.3 From 56c5d418c6ab1a55b676972bbc21c61d957e60f7 Mon Sep 17 00:00:00 2001 From: Gleb Khaykin Date: Tue, 24 Dec 2024 14:33:08 +0700 Subject: [PATCH 07/20] build(docker): setup local supabase --- .pre-commit-config.yaml | 2 - Makefile | 66 +-- cspell.config.js | 3 + eslint.config.js | 2 +- supabase/docker-compose.yaml | 484 ++++++++++++++++++++++ supabase/volumes/api/kong.yml | 241 +++++++++++ supabase/volumes/db/_supabase.sql | 3 + supabase/volumes/db/init/data.sql | 0 supabase/volumes/db/jwt.sql | 5 + supabase/volumes/db/logs.sql | 6 + supabase/volumes/db/pooler.sql | 6 + supabase/volumes/db/realtime.sql | 4 + supabase/volumes/db/roles.sql | 8 + supabase/volumes/db/webhooks.sql | 208 ++++++++++ supabase/volumes/functions/hello/index.ts | 15 + supabase/volumes/functions/main/index.ts | 94 +++++ supabase/volumes/logs/vector.yml | 232 +++++++++++ supabase/volumes/pooler/pooler.exs | 30 ++ 18 files changed, 1378 insertions(+), 31 deletions(-) create mode 100644 supabase/docker-compose.yaml create mode 100644 supabase/volumes/api/kong.yml create mode 100644 supabase/volumes/db/_supabase.sql create mode 100644 supabase/volumes/db/init/data.sql create mode 100644 supabase/volumes/db/jwt.sql create mode 100644 supabase/volumes/db/logs.sql create mode 100644 supabase/volumes/db/pooler.sql create mode 100644 supabase/volumes/db/realtime.sql create mode 100644 supabase/volumes/db/roles.sql create mode 100644 supabase/volumes/db/webhooks.sql create mode 100644 supabase/volumes/functions/hello/index.ts create mode 100644 supabase/volumes/functions/main/index.ts create mode 100644 supabase/volumes/logs/vector.yml create mode 100644 supabase/volumes/pooler/pooler.exs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f146c47..9220bfa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,6 @@ repos: types: [file] files: \.(js|ts|jsx|tsx)$ args: ["--quiet"] - exclude: ^(vendor/|supabase/) - repo: local hooks: @@ -54,7 +53,6 @@ repos: language: system types: [file] files: \.(js|ts|jsx|tsx|css|md|json|yaml)$ - exclude: ^(vendor/|supabase/) - repo: https://github.com/streetsidesoftware/cspell-cli rev: v8.13.3 diff --git a/Makefile b/Makefile index ea2b3a1..7a978d3 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ PLUGINS := \ prerequisites: ## Install prerequisite tools @echo "Checking and installing required plugins." - @echo "$(PLUGINS)" | tr ' ' '\n' | paste - - | while read -r plugin url; do \ + @echo "${PLUGINS}" | tr ' ' '\n' | paste - - | while read -r plugin url; do \ if asdf plugin-list | grep -q $$plugin; then \ echo "Plugin '$$plugin' is already installed."; \ else \ @@ -50,23 +50,23 @@ env: ## Create .env file if it doesn't exist deps-notion: ## Install dependencies for react-notion-x @echo "Installing dependencies for react-notion-x." - git submodule update --init --recursive - cd $(VENDOR_DIR) && yarn install --frozen-lockfile + @git submodule update --init --recursive + @cd ${VENDOR_DIR} && yarn install --frozen-lockfile .PHONY: deps-notion deps: deps-notion ## Install repo dependencies @echo "Installing dependencies." - pnpm install + @pnpm install .PHONY: deps deps-prod: deps-notion ## Install production dependencies @echo "Installing production dependencies." - pnpm install --frozen-lockfile + @pnpm install --frozen-lockfile .PHONY: deps-prod pre-commit: ## Install pre-commit hooks @echo "Installing pre-commit hooks." - pre-commit install -t pre-commit -t commit-msg + @pre-commit install -t pre-commit -t commit-msg .PHONY: pre-commit init: prerequisites env deps pre-commit ## Initialize local environment for development @@ -78,51 +78,51 @@ init: prerequisites env deps pre-commit ## Initialize local environment for deve build-notion: ## Build react-notion-x @echo "Building react-notion-x." - cd $(VENDOR_DIR) && yarn build + @cd ${VENDOR_DIR} && yarn build .PHONY: build-notion build: build-notion ## Build project @echo "Building project." - pnpm run build + @pnpm run build .PHONY: build run-dev: ## Run development server @echo "Running development server." - pnpm run dev + @pnpm run dev .PHONY: run-dev run-prod: build ## Run production server @echo "Starting server." - pnpm run start + @pnpm run start .PHONY: run-prod lint: ## Lint project @echo "Linting project." - pnpm run lint && pnpm run stylelint && pnpm run typecheck + @pnpm run lint && pnpm run stylelint && pnpm run typecheck .PHONY: lint format: ## Format project @echo "Formatting project." - pnpm run format + @pnpm run format .PHONY: format ngrok-dev: ## Run ngrok for development server @echo "Running ngrok." - ngrok http 5173 + @ngrok http 5173 .PHONY: ngrok-dev ngrok-prod: ## Run ngrok for production server @echo "Running ngrok." - ngrok http 55203 + @ngrok http 55203 .PHONY: ngrok-prod ##============================================================================= ##@ Supabase ##============================================================================= -supabase-run: ## Run supabase stack - @echo "Running supabase." - docker compose up -d +supabase-start: ## Start supabase containers + @echo "Starting supabase." + @docker compose --file supabase/docker-compose.yaml up --detach @echo "PG Connection URI: ${POSTGRES_URI}" @echo @echo "JWT Secret: ${JWT_SECRET}" @@ -130,36 +130,46 @@ supabase-run: ## Run supabase stack @echo "Service Key: ${SERVICE_ROLE_KEY}" @echo @echo "API Server: http://localhost:${KONG_HTTP_PORT}/rest/v1/" - @echo "Supabase Studio (Dashboard): http://localhost:${STUDIO_PORT}" -.PHONY: supabase-run + @echo "Supabase Studio: http://localhost:${STUDIO_PORT}" +.PHONY: supabase-start -supabase-stop: ## Stop supabase stack +supabase-stop: ## Stop supabase containers @echo "Stopping supabase." - docker compose down + @docker compose --file supabase/docker-compose.yaml down .PHONY: supabase-stop +supabase-drop: ## Drop supabase database + @echo "Dropping supabase database." + @docker compose --file supabase/docker-compose.yaml down --volumes --remove-orphans +.PHONY: supabase-drop + +supabase-show-config: ## Show supabase config + @echo "Showing supabase config." + @docker compose config +.PHONY: supabase-show-config + ##============================================================================= ##@ Miscellaneous ##============================================================================= create-secrets-baseline: ## Create secrets baseline file - detect-secrets scan > .secrets.baseline + @detect-secrets scan > .secrets.baseline .PHONY: create-secrets-baseline audit-secrets-baseline: ## Check updated .secrets.baseline file - detect-secrets audit .secrets.baseline - git commit .secrets.baseline --no-verify -m "build(security): update secrets.baseline" + @detect-secrets audit .secrets.baseline + @git commit .secrets.baseline --no-verify -m "build(security): update secrets.baseline" .PHONY: audit-secrets-baseline update-pre-commit-hooks: ## Update pre-commit hooks - pre-commit autoupdate + @pre-commit autoupdate .PHONY: update-pre-commit-hooks clean: ## Clean project @echo "Cleaning project." - rm -rf node_modules build - find $(VENDOR_DIR) -type d -name 'build' -exec rm -rf {} + - find $(VENDOR_DIR) -type d -name 'node_modules' -exec rm -rf {} + + @rm -rf node_modules build + @find ${VENDOR_DIR} -type d -name 'build' -exec rm -rf {} + + @find ${VENDOR_DIR} -type d -name 'node_modules' -exec rm -rf {} + .PHONY: clean ##============================================================================= diff --git a/cspell.config.js b/cspell.config.js index 757471b..dc574ff 100644 --- a/cspell.config.js +++ b/cspell.config.js @@ -11,6 +11,8 @@ export default { "node_modules", "package.json", "pnpm-lock.yaml", + "volumes", + "docker-compose.yaml", ], words: [ "workdir", @@ -28,5 +30,6 @@ export default { "pageid", "changefreq", "doesn", + "supabase", ], }; diff --git a/eslint.config.js b/eslint.config.js index a9e0bde..79d18f0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -14,7 +14,7 @@ import globals from "globals"; export default [ // Ignore specific directories { - ignores: ["build/**", "vendor/**"], + ignores: ["build/**", "vendor/**", "supabase/**"], }, // Base config diff --git a/supabase/docker-compose.yaml b/supabase/docker-compose.yaml new file mode 100644 index 0000000..59fdb7e --- /dev/null +++ b/supabase/docker-compose.yaml @@ -0,0 +1,484 @@ +# Usage +# Start: docker compose up +# With helpers: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up +# Stop: docker compose down +# Destroy: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans + +name: supabase + +services: + studio: + container_name: supabase-studio + image: supabase/studio:20241202-71e5240 + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://studio:3000/api/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})", + ] + timeout: 10s + interval: 5s + retries: 3 + depends_on: + analytics: + condition: service_healthy + environment: + STUDIO_PG_META_URL: http://meta:8080 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + + DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION} + DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + + SUPABASE_URL: http://kong:8000 + SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL} + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + AUTH_JWT_SECRET: ${JWT_SECRET} + + LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + LOGFLARE_URL: http://analytics:4000 + NEXT_PUBLIC_ENABLE_LOGS: true + # Comment to use Big Query backend for analytics + NEXT_ANALYTICS_BACKEND_PROVIDER: postgres + # Uncomment to use Big Query backend for analytics + # NEXT_ANALYTICS_BACKEND_PROVIDER: bigquery + ports: + - ${STUDIO_PORT}:3000 + + kong: + container_name: supabase-kong + image: kong:2.8.1 + restart: unless-stopped + # https://unix.stackexchange.com/a/294837 + entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start' + ports: + - ${KONG_HTTP_PORT}:8000/tcp + - ${KONG_HTTPS_PORT}:8443/tcp + depends_on: + analytics: + condition: service_healthy + environment: + KONG_DATABASE: "off" + KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml + # https://github.com/supabase/cli/issues/14 + KONG_DNS_ORDER: LAST,A,CNAME + KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth + KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k + KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + DASHBOARD_USERNAME: ${DASHBOARD_USERNAME} + DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD} + volumes: + # https://github.com/supabase/supabase/issues/12661 + - ./volumes/api/kong.yml:/home/kong/temp.yml:ro + + auth: + container_name: supabase-auth + image: supabase/gotrue:v2.164.0 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:9999/health", + ] + timeout: 5s + interval: 5s + retries: 3 + restart: unless-stopped + environment: + GOTRUE_API_HOST: 0.0.0.0 + GOTRUE_API_PORT: 9999 + API_EXTERNAL_URL: ${API_EXTERNAL_URL} + + GOTRUE_DB_DRIVER: postgres + GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + + GOTRUE_SITE_URL: ${SITE_URL} + GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS} + GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP} + + GOTRUE_JWT_ADMIN_ROLES: service_role + GOTRUE_JWT_AUD: authenticated + GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated + GOTRUE_JWT_EXP: ${JWT_EXPIRY} + GOTRUE_JWT_SECRET: ${JWT_SECRET} + + GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP} + GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS} + GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM} + + # Uncomment to bypass nonce check in ID Token flow. Commonly set to true when using Google Sign In on mobile. + # GOTRUE_EXTERNAL_SKIP_NONCE_CHECK: true + + # GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: true + # GOTRUE_SMTP_MAX_FREQUENCY: 1s + GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL} + GOTRUE_SMTP_HOST: ${SMTP_HOST} + GOTRUE_SMTP_PORT: ${SMTP_PORT} + GOTRUE_SMTP_USER: ${SMTP_USER} + GOTRUE_SMTP_PASS: ${SMTP_PASS} + GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME} + GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE} + GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION} + GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY} + GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE} + + GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP} + GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM} + # Uncomment to enable custom access token hook. Please see: https://supabase.com/docs/guides/auth/auth-hooks for full list of hooks and additional details about custom_access_token_hook + + # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: "true" + # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI: "pg-functions://postgres/public/custom_access_token_hook" + # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS: "" + + # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED: "true" + # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI: "pg-functions://postgres/public/mfa_verification_attempt" + + # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED: "true" + # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI: "pg-functions://postgres/public/password_verification_attempt" + + # GOTRUE_HOOK_SEND_SMS_ENABLED: "false" + # GOTRUE_HOOK_SEND_SMS_URI: "pg-functions://postgres/public/custom_access_token_hook" + # GOTRUE_HOOK_SEND_SMS_SECRETS: "v1,whsec_VGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgc2hvcnRlciBCYXNlNjQgc3RyaW5n" + + # GOTRUE_HOOK_SEND_EMAIL_ENABLED: "false" + # GOTRUE_HOOK_SEND_EMAIL_URI: "http://host.docker.internal:54321/functions/v1/email_sender" + # GOTRUE_HOOK_SEND_EMAIL_SECRETS: "v1,whsec_VGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgc2hvcnRlciBCYXNlNjQgc3RyaW5n" + + rest: + container_name: supabase-rest + image: postgrest/postgrest:v12.2.0 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + restart: unless-stopped + environment: + PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS} + PGRST_DB_ANON_ROLE: anon + PGRST_JWT_SECRET: ${JWT_SECRET} + PGRST_DB_USE_LEGACY_GUCS: "false" + PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET} + PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY} + command: "postgrest" + + realtime: + # This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain + container_name: realtime-dev.supabase-realtime + image: supabase/realtime:v2.33.70 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "curl", + "-sSfL", + "--head", + "-o", + "/dev/null", + "-H", + "Authorization: Bearer ${ANON_KEY}", + "http://localhost:4000/api/tenants/realtime-dev/health", + ] + timeout: 5s + interval: 5s + retries: 3 + restart: unless-stopped + environment: + PORT: 4000 + DB_HOST: ${POSTGRES_HOST} + DB_PORT: ${POSTGRES_PORT} + DB_USER: supabase_admin + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_NAME: ${POSTGRES_DB} + DB_AFTER_CONNECT_QUERY: "SET search_path TO _realtime" + DB_ENC_KEY: supabaserealtime + API_JWT_SECRET: ${JWT_SECRET} + SECRET_KEY_BASE: UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq # pragma: allowlist secret + ERL_AFLAGS: -proto_dist inet_tcp + DNS_NODES: "''" + RLIMIT_NOFILE: "10000" + APP_NAME: realtime + SEED_SELF_HOST: true + RUN_JANITOR: true + + # To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up + storage: + container_name: supabase-storage + image: supabase/storage-api:v1.11.13 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + rest: + condition: service_started + imgproxy: + condition: service_started + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://storage:5000/status", + ] + timeout: 5s + interval: 5s + retries: 3 + restart: unless-stopped + environment: + ANON_KEY: ${ANON_KEY} + SERVICE_KEY: ${SERVICE_ROLE_KEY} + POSTGREST_URL: http://rest:3000 + PGRST_JWT_SECRET: ${JWT_SECRET} + DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + FILE_SIZE_LIMIT: 52428800 + STORAGE_BACKEND: file + FILE_STORAGE_BACKEND_PATH: /var/lib/storage + TENANT_ID: stub + # TODO: https://github.com/supabase/storage-api/issues/55 + REGION: stub + GLOBAL_S3_BUCKET: stub + ENABLE_IMAGE_TRANSFORMATION: "true" + IMGPROXY_URL: http://imgproxy:5001 + volumes: + - ./volumes/storage:/var/lib/storage:z + + imgproxy: + container_name: supabase-imgproxy + image: darthsim/imgproxy:v3.8.0 + healthcheck: + test: ["CMD", "imgproxy", "health"] + timeout: 5s + interval: 5s + retries: 3 + environment: + IMGPROXY_BIND: ":5001" + IMGPROXY_LOCAL_FILESYSTEM_ROOT: / + IMGPROXY_USE_ETAG: "true" + IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION} + volumes: + - ./volumes/storage:/var/lib/storage:z + + meta: + container_name: supabase-meta + image: supabase/postgres-meta:v0.84.2 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + restart: unless-stopped + environment: + PG_META_PORT: 8080 + PG_META_DB_HOST: ${POSTGRES_HOST} + PG_META_DB_PORT: ${POSTGRES_PORT} + PG_META_DB_NAME: ${POSTGRES_DB} + PG_META_DB_USER: supabase_admin + PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} + + functions: + container_name: supabase-edge-functions + image: supabase/edge-runtime:v1.65.3 + restart: unless-stopped + depends_on: + analytics: + condition: service_healthy + environment: + JWT_SECRET: ${JWT_SECRET} + SUPABASE_URL: http://kong:8000 + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY} + SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + # TODO: Allow configuring VERIFY_JWT per function. This PR might help: https://github.com/supabase/cli/pull/786 + VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}" + volumes: + - ./volumes/functions:/home/deno/functions:Z + command: + - start + - --main-service + - /home/deno/functions/main + + analytics: + container_name: supabase-analytics + image: supabase/logflare:1.4.0 + healthcheck: + test: ["CMD", "curl", "http://localhost:4000/health"] + timeout: 5s + interval: 5s + retries: 10 + restart: unless-stopped + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + # Uncomment to use Big Query backend for analytics + # volumes: + # - type: bind + # source: ${PWD}/gcloud.json + # target: /opt/app/rel/logflare/bin/gcloud.json + # read_only: true + environment: + LOGFLARE_NODE_HOST: 127.0.0.1 + DB_USERNAME: supabase_admin + DB_DATABASE: _supabase + DB_HOSTNAME: ${POSTGRES_HOST} + DB_PORT: ${POSTGRES_PORT} + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_SCHEMA: _analytics + LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + LOGFLARE_SINGLE_TENANT: true + LOGFLARE_SUPABASE_MODE: true + LOGFLARE_MIN_CLUSTER_SIZE: 1 + + # Comment variables to use Big Query backend for analytics + POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase + POSTGRES_BACKEND_SCHEMA: _analytics + LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true + # Uncomment to use Big Query backend for analytics + # GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID} + # GOOGLE_PROJECT_NUMBER: ${GOOGLE_PROJECT_NUMBER} + ports: + - 4000:4000 + + # Comment out everything below this point if you are using an external Postgres database + db: + container_name: supabase-db + image: supabase/postgres:15.6.1.146 + healthcheck: + test: pg_isready -U postgres -h localhost + interval: 5s + timeout: 5s + retries: 10 + depends_on: + vector: + condition: service_healthy + command: + - postgres + - -c + - config_file=/etc/postgresql/postgresql.conf + - -c + - log_min_messages=fatal # prevents Realtime polling queries from appearing in logs + restart: unless-stopped + environment: + POSTGRES_HOST: /var/run/postgresql + PGPORT: ${POSTGRES_PORT} + POSTGRES_PORT: ${POSTGRES_PORT} + PGPASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PGDATABASE: ${POSTGRES_DB} + POSTGRES_DB: ${POSTGRES_DB} + JWT_SECRET: ${JWT_SECRET} + JWT_EXP: ${JWT_EXPIRY} + volumes: + - ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z + # Must be superuser to create event trigger + - ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z + # Must be superuser to alter reserved role + - ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z + # Initialize the database settings with JWT_SECRET and JWT_EXP + - ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z + # PGDATA directory is persisted between restarts + - ./volumes/db/data:/var/lib/postgresql/data:Z + # Changes required for internal supabase data such as _analytics + - ./volumes/db/_supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:Z + # Changes required for Analytics support + - ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z + # Changes required for Pooler support + - ./volumes/db/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:Z + # Use named volume to persist pgsodium decryption key between restarts + - db-config:/etc/postgresql-custom + + vector: + container_name: supabase-vector + image: timberio/vector:0.28.1-alpine + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://vector:9001/health", + ] + timeout: 5s + interval: 5s + retries: 3 + volumes: + - ./volumes/logs/vector.yml:/etc/vector/vector.yml:ro + - ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro + environment: + LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + command: ["--config", "/etc/vector/vector.yml"] + + # Update the DATABASE_URL if you are using an external Postgres database + supavisor: + container_name: supabase-pooler + image: supabase/supavisor:1.1.56 + healthcheck: + test: curl -sSfL --head -o /dev/null "http://127.0.0.1:4000/api/health" + interval: 10s + timeout: 5s + retries: 5 + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + command: + - /bin/sh + - -c + - /app/bin/migrate && /app/bin/supavisor eval "$$(cat /etc/pooler/pooler.exs)" && /app/bin/server + restart: unless-stopped + ports: + - ${POSTGRES_PORT}:5432 + - ${POOLER_PROXY_PORT_TRANSACTION}:6543 + environment: + - PORT=4000 + - POSTGRES_PORT=${POSTGRES_PORT} + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - DATABASE_URL=ecto://supabase_admin:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/_supabase + - CLUSTER_POSTGRES=true + - SECRET_KEY_BASE=UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq + - VAULT_ENC_KEY=your-encryption-key-32-chars-min + - API_JWT_SECRET=${JWT_SECRET} + - METRICS_JWT_SECRET=${JWT_SECRET} + - REGION=local + - ERL_AFLAGS=-proto_dist inet_tcp + - POOLER_TENANT_ID=${POOLER_TENANT_ID} + - POOLER_DEFAULT_POOL_SIZE=${POOLER_DEFAULT_POOL_SIZE} + - POOLER_MAX_CLIENT_CONN=${POOLER_MAX_CLIENT_CONN} + - POOLER_POOL_MODE=transaction + volumes: + - ./volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro + +volumes: + db-config: diff --git a/supabase/volumes/api/kong.yml b/supabase/volumes/api/kong.yml new file mode 100644 index 0000000..28d3215 --- /dev/null +++ b/supabase/volumes/api/kong.yml @@ -0,0 +1,241 @@ +_format_version: "2.1" +_transform: true + +### +### Consumers / Users +### +consumers: + - username: DASHBOARD + - username: anon + keyauth_credentials: + - key: $SUPABASE_ANON_KEY + - username: service_role + keyauth_credentials: + - key: $SUPABASE_SERVICE_KEY + +### +### Access Control List +### +acls: + - consumer: anon + group: anon + - consumer: service_role + group: admin + +### +### Dashboard credentials +### +basicauth_credentials: + - consumer: DASHBOARD + username: $DASHBOARD_USERNAME + password: $DASHBOARD_PASSWORD + +### +### API Routes +### +services: + ## Open Auth routes + - name: auth-v1-open + url: http://auth:9999/verify + routes: + - name: auth-v1-open + strip_path: true + paths: + - /auth/v1/verify + plugins: + - name: cors + - name: auth-v1-open-callback + url: http://auth:9999/callback + routes: + - name: auth-v1-open-callback + strip_path: true + paths: + - /auth/v1/callback + plugins: + - name: cors + - name: auth-v1-open-authorize + url: http://auth:9999/authorize + routes: + - name: auth-v1-open-authorize + strip_path: true + paths: + - /auth/v1/authorize + plugins: + - name: cors + + ## Secure Auth routes + - name: auth-v1 + _comment: "GoTrue: /auth/v1/* -> http://auth:9999/*" + url: http://auth:9999/ + routes: + - name: auth-v1-all + strip_path: true + paths: + - /auth/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure REST routes + - name: rest-v1 + _comment: "PostgREST: /rest/v1/* -> http://rest:3000/*" + url: http://rest:3000/ + routes: + - name: rest-v1-all + strip_path: true + paths: + - /rest/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure GraphQL routes + - name: graphql-v1 + _comment: "PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql" + url: http://rest:3000/rpc/graphql + routes: + - name: graphql-v1-all + strip_path: true + paths: + - /graphql/v1 + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + - name: request-transformer + config: + add: + headers: + - Content-Profile:graphql_public + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure Realtime routes + - name: realtime-v1-ws + _comment: "Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*" + url: http://realtime-dev.supabase-realtime:4000/socket + protocol: ws + routes: + - name: realtime-v1-ws + strip_path: true + paths: + - /realtime/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + - name: realtime-v1-rest + _comment: "Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*" + url: http://realtime-dev.supabase-realtime:4000/api + protocol: http + routes: + - name: realtime-v1-rest + strip_path: true + paths: + - /realtime/v1/api + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + ## Storage routes: the storage server manages its own auth + - name: storage-v1 + _comment: "Storage: /storage/v1/* -> http://storage:5000/*" + url: http://storage:5000/ + routes: + - name: storage-v1-all + strip_path: true + paths: + - /storage/v1/ + plugins: + - name: cors + + ## Edge Functions routes + - name: functions-v1 + _comment: "Edge Functions: /functions/v1/* -> http://functions:9000/*" + url: http://functions:9000/ + routes: + - name: functions-v1-all + strip_path: true + paths: + - /functions/v1/ + plugins: + - name: cors + + ## Analytics routes + - name: analytics-v1 + _comment: "Analytics: /analytics/v1/* -> http://logflare:4000/*" + url: http://analytics:4000/ + routes: + - name: analytics-v1-all + strip_path: true + paths: + - /analytics/v1/ + + ## Secure Database routes + - name: meta + _comment: "pg-meta: /pg/* -> http://pg-meta:8080/*" + url: http://meta:8080/ + routes: + - name: meta-all + strip_path: true + paths: + - /pg/ + plugins: + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + + ## Protected Dashboard - catch all remaining routes + - name: dashboard + _comment: "Studio: /* -> http://studio:3000/*" + url: http://studio:3000/ + routes: + - name: dashboard-all + strip_path: true + paths: + - / + plugins: + - name: cors + - name: basic-auth + config: + hide_credentials: true diff --git a/supabase/volumes/db/_supabase.sql b/supabase/volumes/db/_supabase.sql new file mode 100644 index 0000000..6236ae1 --- /dev/null +++ b/supabase/volumes/db/_supabase.sql @@ -0,0 +1,3 @@ +\set pguser `echo "$POSTGRES_USER"` + +CREATE DATABASE _supabase WITH OWNER :pguser; diff --git a/supabase/volumes/db/init/data.sql b/supabase/volumes/db/init/data.sql new file mode 100644 index 0000000..e69de29 diff --git a/supabase/volumes/db/jwt.sql b/supabase/volumes/db/jwt.sql new file mode 100644 index 0000000..cfd3b16 --- /dev/null +++ b/supabase/volumes/db/jwt.sql @@ -0,0 +1,5 @@ +\set jwt_secret `echo "$JWT_SECRET"` +\set jwt_exp `echo "$JWT_EXP"` + +ALTER DATABASE postgres SET "app.settings.jwt_secret" TO :'jwt_secret'; +ALTER DATABASE postgres SET "app.settings.jwt_exp" TO :'jwt_exp'; diff --git a/supabase/volumes/db/logs.sql b/supabase/volumes/db/logs.sql new file mode 100644 index 0000000..255c0f4 --- /dev/null +++ b/supabase/volumes/db/logs.sql @@ -0,0 +1,6 @@ +\set pguser `echo "$POSTGRES_USER"` + +\c _supabase +create schema if not exists _analytics; +alter schema _analytics owner to :pguser; +\c postgres diff --git a/supabase/volumes/db/pooler.sql b/supabase/volumes/db/pooler.sql new file mode 100644 index 0000000..162c5b9 --- /dev/null +++ b/supabase/volumes/db/pooler.sql @@ -0,0 +1,6 @@ +\set pguser `echo "$POSTGRES_USER"` + +\c _supabase +create schema if not exists _supavisor; +alter schema _supavisor owner to :pguser; +\c postgres diff --git a/supabase/volumes/db/realtime.sql b/supabase/volumes/db/realtime.sql new file mode 100644 index 0000000..4d4b9ff --- /dev/null +++ b/supabase/volumes/db/realtime.sql @@ -0,0 +1,4 @@ +\set pguser `echo "$POSTGRES_USER"` + +create schema if not exists _realtime; +alter schema _realtime owner to :pguser; diff --git a/supabase/volumes/db/roles.sql b/supabase/volumes/db/roles.sql new file mode 100644 index 0000000..8f7161a --- /dev/null +++ b/supabase/volumes/db/roles.sql @@ -0,0 +1,8 @@ +-- NOTE: change to your own passwords for production environments +\set pgpass `echo "$POSTGRES_PASSWORD"` + +ALTER USER authenticator WITH PASSWORD :'pgpass'; +ALTER USER pgbouncer WITH PASSWORD :'pgpass'; +ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass'; +ALTER USER supabase_functions_admin WITH PASSWORD :'pgpass'; +ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass'; diff --git a/supabase/volumes/db/webhooks.sql b/supabase/volumes/db/webhooks.sql new file mode 100644 index 0000000..5837b86 --- /dev/null +++ b/supabase/volumes/db/webhooks.sql @@ -0,0 +1,208 @@ +BEGIN; + -- Create pg_net extension + CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions; + -- Create supabase_functions schema + CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin; + GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role; + -- supabase_functions.migrations definition + CREATE TABLE supabase_functions.migrations ( + version text PRIMARY KEY, + inserted_at timestamptz NOT NULL DEFAULT NOW() + ); + -- Initial supabase_functions migration + INSERT INTO supabase_functions.migrations (version) VALUES ('initial'); + -- supabase_functions.hooks definition + CREATE TABLE supabase_functions.hooks ( + id bigserial PRIMARY KEY, + hook_table_id integer NOT NULL, + hook_name text NOT NULL, + created_at timestamptz NOT NULL DEFAULT NOW(), + request_id bigint + ); + CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id); + CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name); + COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.'; + CREATE FUNCTION supabase_functions.http_request() + RETURNS trigger + LANGUAGE plpgsql + AS $function$ + DECLARE + request_id bigint; + payload jsonb; + url text := TG_ARGV[0]::text; + method text := TG_ARGV[1]::text; + headers jsonb DEFAULT '{}'::jsonb; + params jsonb DEFAULT '{}'::jsonb; + timeout_ms integer DEFAULT 1000; + BEGIN + IF url IS NULL OR url = 'null' THEN + RAISE EXCEPTION 'url argument is missing'; + END IF; + + IF method IS NULL OR method = 'null' THEN + RAISE EXCEPTION 'method argument is missing'; + END IF; + + IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN + headers = '{"Content-Type": "application/json"}'::jsonb; + ELSE + headers = TG_ARGV[2]::jsonb; + END IF; + + IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN + params = '{}'::jsonb; + ELSE + params = TG_ARGV[3]::jsonb; + END IF; + + IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN + timeout_ms = 1000; + ELSE + timeout_ms = TG_ARGV[4]::integer; + END IF; + + CASE + WHEN method = 'GET' THEN + SELECT http_get INTO request_id FROM net.http_get( + url, + params, + headers, + timeout_ms + ); + WHEN method = 'POST' THEN + payload = jsonb_build_object( + 'old_record', OLD, + 'record', NEW, + 'type', TG_OP, + 'table', TG_TABLE_NAME, + 'schema', TG_TABLE_SCHEMA + ); + + SELECT http_post INTO request_id FROM net.http_post( + url, + payload, + params, + headers, + timeout_ms + ); + ELSE + RAISE EXCEPTION 'method argument % is invalid', method; + END CASE; + + INSERT INTO supabase_functions.hooks + (hook_table_id, hook_name, request_id) + VALUES + (TG_RELID, TG_NAME, request_id); + + RETURN NEW; + END + $function$; + -- Supabase super admin + DO + $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_roles + WHERE rolname = 'supabase_functions_admin' + ) + THEN + CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION; + END IF; + END + $$; + GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin; + GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin; + GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin; + ALTER USER supabase_functions_admin SET search_path = "supabase_functions"; + ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin; + ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin; + ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin; + GRANT supabase_functions_admin TO postgres; + -- Remove unused supabase_pg_net_admin role + DO + $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_roles + WHERE rolname = 'supabase_pg_net_admin' + ) + THEN + REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin; + DROP OWNED BY supabase_pg_net_admin; + DROP ROLE supabase_pg_net_admin; + END IF; + END + $$; + -- pg_net grants when extension is already enabled + DO + $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_extension + WHERE extname = 'pg_net' + ) + THEN + GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + END IF; + END + $$; + -- Event trigger for pg_net + CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access() + RETURNS event_trigger + LANGUAGE plpgsql + AS $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_event_trigger_ddl_commands() AS ev + JOIN pg_extension AS ext + ON ev.objid = ext.oid + WHERE ext.extname = 'pg_net' + ) + THEN + GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + END IF; + END; + $$; + COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net'; + DO + $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_event_trigger + WHERE evtname = 'issue_pg_net_access' + ) THEN + CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION') + EXECUTE PROCEDURE extensions.grant_pg_net_access(); + END IF; + END + $$; + INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants'); + ALTER function supabase_functions.http_request() SECURITY DEFINER; + ALTER function supabase_functions.http_request() SET search_path = supabase_functions; + REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC; + GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role; +COMMIT; diff --git a/supabase/volumes/functions/hello/index.ts b/supabase/volumes/functions/hello/index.ts new file mode 100644 index 0000000..0447d32 --- /dev/null +++ b/supabase/volumes/functions/hello/index.ts @@ -0,0 +1,15 @@ +// Follow this setup guide to integrate the Deno language server with your editor: +// https://deno.land/manual/getting_started/setup_your_environment +// This enables autocomplete, go to definition, etc. + +import { serve } from "https://deno.land/std@0.177.1/http/server.ts"; + +serve(async () => { + return new Response(`"Hello from Edge Functions!"`, { + headers: { "Content-Type": "application/json" }, + }); +}); + +// To invoke: +// curl 'http://localhost:/functions/v1/hello' \ +// --header 'Authorization: Bearer ' diff --git a/supabase/volumes/functions/main/index.ts b/supabase/volumes/functions/main/index.ts new file mode 100644 index 0000000..f406e21 --- /dev/null +++ b/supabase/volumes/functions/main/index.ts @@ -0,0 +1,94 @@ +import { serve } from "https://deno.land/std@0.131.0/http/server.ts"; +import * as jose from "https://deno.land/x/jose@v4.14.4/index.ts"; + +console.log("main function started"); + +const JWT_SECRET = Deno.env.get("JWT_SECRET"); +const VERIFY_JWT = Deno.env.get("VERIFY_JWT") === "true"; + +function getAuthToken(req: Request) { + const authHeader = req.headers.get("authorization"); + if (!authHeader) { + throw new Error("Missing authorization header"); + } + const [bearer, token] = authHeader.split(" "); + if (bearer !== "Bearer") { + throw new Error(`Auth header is not 'Bearer {token}'`); + } + return token; +} + +async function verifyJWT(jwt: string): Promise { + const encoder = new TextEncoder(); + const secretKey = encoder.encode(JWT_SECRET); + try { + await jose.jwtVerify(jwt, secretKey); + } catch (err) { + console.error(err); + return false; + } + return true; +} + +serve(async (req: Request) => { + if (req.method !== "OPTIONS" && VERIFY_JWT) { + try { + const token = getAuthToken(req); + const isValidJWT = await verifyJWT(token); + + if (!isValidJWT) { + return new Response(JSON.stringify({ msg: "Invalid JWT" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } + } catch (e) { + console.error(e); + return new Response(JSON.stringify({ msg: e.toString() }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } + } + + const url = new URL(req.url); + const { pathname } = url; + const path_parts = pathname.split("/"); + const service_name = path_parts[1]; + + if (!service_name || service_name === "") { + const error = { msg: "missing function name in request" }; + return new Response(JSON.stringify(error), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + const servicePath = `/home/deno/functions/${service_name}`; + console.error(`serving the request with ${servicePath}`); + + const memoryLimitMb = 150; + const workerTimeoutMs = 1 * 60 * 1000; + const noModuleCache = false; + const importMapPath = null; + const envVarsObj = Deno.env.toObject(); + const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]]); + + try { + const worker = await EdgeRuntime.userWorkers.create({ + servicePath, + memoryLimitMb, + workerTimeoutMs, + noModuleCache, + importMapPath, + envVars, + }); + return await worker.fetch(req); + } catch (e) { + const error = { msg: e.toString() }; + return new Response(JSON.stringify(error), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } +}); diff --git a/supabase/volumes/logs/vector.yml b/supabase/volumes/logs/vector.yml new file mode 100644 index 0000000..0deacde --- /dev/null +++ b/supabase/volumes/logs/vector.yml @@ -0,0 +1,232 @@ +api: + enabled: true + address: 0.0.0.0:9001 + +sources: + docker_host: + type: docker_logs + exclude_containers: + - supabase-vector + +transforms: + project_logs: + type: remap + inputs: + - docker_host + source: |- + .project = "default" + .event_message = del(.message) + .appname = del(.container_name) + del(.container_created_at) + del(.container_id) + del(.source_type) + del(.stream) + del(.label) + del(.image) + del(.host) + del(.stream) + router: + type: route + inputs: + - project_logs + route: + kong: '.appname == "supabase-kong"' + auth: '.appname == "supabase-auth"' + rest: '.appname == "supabase-rest"' + realtime: '.appname == "supabase-realtime"' + storage: '.appname == "supabase-storage"' + functions: '.appname == "supabase-functions"' + db: '.appname == "supabase-db"' + # Ignores non nginx errors since they are related with kong booting up + kong_logs: + type: remap + inputs: + - router.kong + source: |- + req, err = parse_nginx_log(.event_message, "combined") + if err == null { + .timestamp = req.timestamp + .metadata.request.headers.referer = req.referer + .metadata.request.headers.user_agent = req.agent + .metadata.request.headers.cf_connecting_ip = req.client + .metadata.request.method = req.method + .metadata.request.path = req.path + .metadata.request.protocol = req.protocol + .metadata.response.status_code = req.status + } + if err != null { + abort + } + # Ignores non nginx errors since they are related with kong booting up + kong_err: + type: remap + inputs: + - router.kong + source: |- + .metadata.request.method = "GET" + .metadata.response.status_code = 200 + parsed, err = parse_nginx_log(.event_message, "error") + if err == null { + .timestamp = parsed.timestamp + .severity = parsed.severity + .metadata.request.host = parsed.host + .metadata.request.headers.cf_connecting_ip = parsed.client + url, err = split(parsed.request, " ") + if err == null { + .metadata.request.method = url[0] + .metadata.request.path = url[1] + .metadata.request.protocol = url[2] + } + } + if err != null { + abort + } + # Gotrue logs are structured json strings which frontend parses directly. But we keep metadata for consistency. + auth_logs: + type: remap + inputs: + - router.auth + source: |- + parsed, err = parse_json(.event_message) + if err == null { + .metadata.timestamp = parsed.time + .metadata = merge!(.metadata, parsed) + } + # PostgREST logs are structured so we separate timestamp from message using regex + rest_logs: + type: remap + inputs: + - router.rest + source: |- + parsed, err = parse_regex(.event_message, r'^(?P