Make local DEV work out of the box

This commit is contained in:
MartinBraquet
2025-09-20 23:51:28 +02:00
parent d7c95e2ae0
commit 84a437772d
22 changed files with 138 additions and 121 deletions

View File

@@ -1,28 +1,11 @@
# Rename this file to `.env` and fill in the values.
# You already have access to basic local functionality (UI, authentication, database read access).
# Optional variables for the backend server functionality (modifying user data, etc.)
# For Firebase access.
# Open a GitHub issue with your contribution ideas and an admin will give you the key.
# TODO: find a way to give anyone moderate access to dev firebase.
GOOGLE_APPLICATION_CREDENTIALS_DEV="[...].json"
# The URL where your local backend server is running.
# You can change the port if needed.
NEXT_PUBLIC_API_URL=localhost:8088
# Optional variables for full local functionality
# For the location / distance filtering features.
# Create a free account at https://rapidapi.com/wirefreethought/api/geodb-cities and get an API key.
GEODB_API_KEY=
# For analytics like page views, user actions, feature usage, etc.
# Create a free account at https://posthog.com and get a project API key. Should start with "phc_".
POSTHOG_KEY=
# For sending emails (e.g. for user sign up, password reset, notifications, etc.).
# Create a free account at https://resend.com and get an API key. Should start with "re_".
RESEND_API_KEY=
RESEND_KEY=

View File

@@ -100,6 +100,8 @@ yarn install
### Environment Variables
Almost all the features will work out of the box, so you can skip this step and come back later if you need to test the following services: email, geolocation.
We can't make the following information public, for security and privacy reasons:
- Database, otherwise anyone could access all the user data (including private messages)
- Firebase, otherwise anyone could remove users or modify the media files
@@ -108,12 +110,7 @@ We can't make the following information public, for security and privacy reasons
We separate all those services between production and local development, so that you can code freely without impacting the functioning of the platform.
Contributors should use the default keys for local development. Production uses a separate environment with stricter rules and private keys that are not shared.
Most of the code will work out of the box. All you need to do is creating an `.env` file as a copy of `.env.example`:
```bash
cp .env.example .env
```
If you do need one of the few remaining services, you need to store your own secrets as environment variables. To do so, simply open `.env` and fill in the variables according to the instructions in the file.
If you do need one of the few remaining services, you need to set them up and store your own secrets as environment variables. To do so, simply open `.env` and fill in the variables according to the instructions in the file.
### Tests

View File

@@ -1,14 +1,14 @@
import * as admin from 'firebase-admin'
import {getLocalEnv, initAdmin} from 'shared/init-admin'
import {loadSecretsToEnv, getServiceAccountCredentials} from 'common/secrets'
import {initAdmin} from 'shared/init-admin'
import {loadSecretsToEnv} from 'common/secrets'
import {log} from 'shared/utils'
import {LOCAL_DEV} from "common/envs/constants";
import {IS_LOCAL} from "common/envs/constants";
import {METRIC_WRITER} from 'shared/monitoring/metric-writer'
import {listen as webSocketListen} from 'shared/websockets/server'
log('Api server starting up....')
if (LOCAL_DEV) {
if (IS_LOCAL) {
initAdmin()
} else {
const projectId = process.env.GOOGLE_CLOUD_PROJECT
@@ -21,9 +21,10 @@ if (LOCAL_DEV) {
METRIC_WRITER.start()
import {app} from './app'
import {getServiceAccountCredentials} from "shared/firebase-utils";
const credentials = LOCAL_DEV
? getServiceAccountCredentials(getLocalEnv())
const credentials = IS_LOCAL
? getServiceAccountCredentials()
: // No explicit credentials needed for deployed service.
undefined
@@ -37,6 +38,5 @@ const startupProcess = async () => {
})
webSocketListen(httpServer, '/ws')
log('Server started successfully')
}
startupProcess()
startupProcess().then(r => log('Server started successfully'))

View File

@@ -20,6 +20,7 @@ export const sendEmail = async (
console.log(resend, payload, options)
async function sendEmailThrottle(data: any, options: any) {
if (!resend) return { data: null, error: 'No Resend client' }
return limit(() => resend.emails.send(data, options))
}
@@ -45,6 +46,11 @@ let resend: Resend | null = null
const getResend = () => {
if (resend) return resend
if (!process.env.RESEND_KEY) {
console.log('No RESEND_KEY, skipping email send')
return
}
const apiKey = process.env.RESEND_KEY as string
// console.log(`RESEND_KEY: ${apiKey}`)
resend = new Resend(apiKey)

View File

@@ -1,22 +1,19 @@
import { getLocalEnv, initAdmin } from 'shared/init-admin'
import { getServiceAccountCredentials, loadSecretsToEnv } from 'common/secrets'
import {
createSupabaseDirectClient,
type SupabaseDirectClient,
} from 'shared/supabase/init'
import {initAdmin} from 'shared/init-admin'
import {loadSecretsToEnv} from 'common/secrets'
import {createSupabaseDirectClient, type SupabaseDirectClient,} from 'shared/supabase/init'
import {getServiceAccountCredentials} from "shared/firebase-utils";
initAdmin()
export const runScript = async (
main: (services: { pg: SupabaseDirectClient }) => Promise<any> | any
) => {
const env = getLocalEnv()
const credentials = getServiceAccountCredentials(env)
const credentials = getServiceAccountCredentials()
await loadSecretsToEnv(credentials)
const pg = createSupabaseDirectClient()
await main({ pg })
await main({pg})
process.exit()
}

View File

@@ -0,0 +1,3 @@
export const getLocalEnv = () => {
return (process.env.ENVIRONMENT?.toUpperCase() ?? 'DEV') as 'PROD' | 'DEV'
}

View File

@@ -0,0 +1,23 @@
import {readFileSync} from "fs";
import {ENV_CONFIG} from "common/envs/constants";
export const getServiceAccountCredentials = () => {
let keyPath = ENV_CONFIG.googleApplicationCredentials
if (keyPath == null) {
throw new Error(
`Please set the GOOGLE_APPLICATION_CREDENTIALS environment variable to contain the path to your key file.`
)
}
if (!keyPath.startsWith('/')) {
// Make relative paths relative to the current file
keyPath = __dirname + '/' + keyPath
// console.log(keyPath)
}
try {
return JSON.parse(readFileSync(keyPath, {encoding: 'utf8'}))
} catch (e) {
throw new Error(`Failed to load service account key from ${keyPath}: ${e}`)
}
}

View File

@@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "compass-57c3c",
"private_key_id": "ce2c7ef4aa137dd4726a8c9398c199ce051d4168",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCNkwHW3buaXX8K\nYWZMIPtHfObK1EmbsIbjbFZtWqcn1Ouw3nj/hCaQQhFmlAvGIP/P0A6d/mlr704o\nK26otjK3S290y/VhI3f2AoMh9af8ptK9mjMbmpwQI7cRu87M/EhbBlYeovntcNfj\n11tsMg83RYUIPXKuQddm2AejRDyhoHsB/QTdSLjDfUIR2pVvHSVbbQwm1cKiFAev\nPm01X3BFLOkzhUCYuzLI1hGXf7G4xm4XCTi2UQGZkI2FYjOgrQcNvkTB33DF0sze\nZCpc8iJsUyQYZxxvsxXd20CwPArsgy+6FU2+dGRrSE6lzwqYU9mnizqtjdp+VUh8\nkppRgZH7AgMBAAECggEAFxMKPjR2grLRZWY5j5fijKTBUvalpp/vZDrAnWMkklvk\nLDgeXXry9Bkoj+D6SEkRmJPPBhY0pXhj8y0dBJdpjbFYUZ96d2IaB7kiGVNaFVY1\nS9zJjqq02/aOPHAxRPyraFaQi77BYF8/eK2dg3VnQHlutMibG+a0TllQaV5SSX9J\n7cj4C6RX2p8Zvrmu6RsQ+dQWZFMwT8oHhuKCrL8+iw6bXYH1bSvxHLS+sCreIK7d\nYvY/DPxlC1rGaK8ovH5nuc3nQ+ECsWcDjcqjg4SmI7VwPpWeJ97exf3BGUmSFSOC\nCNfWu6bqhWleBkRozRCAuzQwesLkr38MsjU1iZS4VQKBgQDAG75trRLUHvuCoPpu\nHw4Ev1qpqVa1rE/16Zmzp7/wzoOP7yqoW2Og2adE5N2KUxR48nAMnR/uwRQcvlhX\n2Y7qEprl0AkXRXpneGiPFD/vzkTAjRWZdd0sIpo3+KCZcoO9OsJ8eA5Ch0bthv6q\nJBjd9VwgIaS5gd/eX76xBMV6ZQKBgQC8qL+bc6S3EiGoGf+Fzm+XXfXuVn0VA2sx\nhJjV/tfxxv6EIO90COsKy3CgaNOjU+NZ6jx+Kq07c4HBPA3c43qSRNUkWfQyyQgA\nPV3f5ZP40z+U4QnbhWSVSch91FCxwAnL9N7/KM77gAsHCdUuMlCVAYDr6JfDZiJR\nQ9X3cfLk3wKBgHn4o31rJ8s6KKIVpysH2JS3Ec8qzvzl/Ja7zHS+iyVPWUSnq0Pd\nUnIr/wHE9cv/V746312C3WVvfV+KkvikDxMa4PIMldkKqd7MGkbNqpKNOiWu7gnT\nRaviBFyJJR6IEJCyoAz7BMLEtQnWbhaEeK1kPSvBcJ6/kO3ViHNH/kHpAoGBAJ2Z\nLi8TBOc1y03dIfrKP6goAtiuAWF7cKF2DiK99/DudhE0XjQFeyuSVSx7RUisPEER\njqUqy3ndfOhKXZ5HnU3xGEh8qKWAECH7IZ927gyvk+6vqwdpwGOBtm1+3kYOkWCC\n14I5ueaYyR2BFke4Gl7PWb44mAbQHBzc2TITS3/rAoGBAKsCx9IQk1p0Am/+/NcW\nBnxnE5iNmaX3H4+pmtShl8gXxaI1DWnB7WbqjDOCu9HCnPZUdLXbrserny+ZSyNU\nnHE1InQn7T/xM7RXZRkZk09qmSRicXh6kS+cSBg1xU5vIPPsBTrWVmg74L9guNdo\nYQXUSGJaJbCZ6N35oz+Qf3NB\n-----END PRIVATE KEY-----\n",
"client_email": "dev-contributors@compass-57c3c.iam.gserviceaccount.com",
"client_id": "118371540020807340605",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/dev-contributors%40compass-57c3c.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View File

@@ -1,16 +1,12 @@
import * as admin from 'firebase-admin'
import { getServiceAccountCredentials } from 'common/secrets'
export const getLocalEnv = () => {
return (process.env.ENV?.toUpperCase() ?? 'STAGING') as 'PROD' | 'DEV'
}
import {getServiceAccountCredentials} from "shared/firebase-utils";
// Locally initialize Firebase Admin.
export const initAdmin = () => {
try {
const env = getLocalEnv()
const serviceAccount = getServiceAccountCredentials(env)
const serviceAccount = getServiceAccountCredentials()
console.log(
`Initializing connection to ${serviceAccount.project_id} Firebase...`
)

View File

@@ -36,11 +36,11 @@ export type SupabaseTransaction = ITask<{}>
export type SupabaseDirectClient = IDatabase<{}, IClient> | SupabaseTransaction
export function getInstanceId() {
return process.env.SUPABASE_INSTANCE_ID ?? ENV_CONFIG.supabaseInstanceId
return ENV_CONFIG.supabaseInstanceId
}
export function getSupabasePwd() {
return ENV_CONFIG.supabaseServiceRoleKey ?? process.env.SUPABASE_DB_PASSWORD
return ENV_CONFIG.supabasePwd
}
const newClient = (
@@ -55,7 +55,7 @@ const newClient = (
// This host is IPV4 compatible, for the google cloud VM
host: 'aws-1-us-west-1.pooler.supabase.com',
port: 5432,
user: `postgres.ltzepxnhhnrnvovqblfr`,
user: `postgres.${instanceId}`,
password: password,
database: 'postgres',
pool_mode: 'session',

View File

@@ -9,7 +9,7 @@ import {
ServerMessage,
CLIENT_MESSAGE_SCHEMA,
} from 'common/api/websockets'
import {LOCAL_DEV} from "common/envs/constants";
import {IS_LOCAL} from "common/envs/constants";
const SWITCHBOARD = new Switchboard()
@@ -107,7 +107,7 @@ export function broadcastMulti(topics: string[], data: BroadcastPayload) {
// it isn't secure to do this in prod for auth reasons (maybe?)
// but it's super convenient for testing
if (LOCAL_DEV) {
if (IS_LOCAL) {
const msg = { type: 'broadcast', topic: '*', topics, data }
sendToSubscribers('*', msg)
}

View File

@@ -9,7 +9,7 @@ regen-types-prod:
cd ../../common && npx prettier --write ./src/supabase/schema.ts
regen-types-dev:
npx supabase gen types typescript --project-id ltzepxnhhnrnvovqblfr --schema public > ../../common/src/supabase/schema.ts
npx supabase gen types typescript --project-id zbspxezubpzxmuxciurg --schema public > ../../common/src/supabase/schema.ts
cd ../../common && npx prettier --write ./src/supabase/schema.ts
regen-schema:

View File

@@ -1,4 +1,4 @@
import { ENV_CONFIG } from 'common/envs/constants'
import {BACKEND_DOMAIN} from 'common/envs/constants'
type ErrorCode =
| 400 // your input is bad (like zod is mad)
@@ -11,6 +11,7 @@ type ErrorCode =
export class APIError extends Error {
code: ErrorCode
details?: unknown
constructor(code: ErrorCode, message: string, details?: unknown) {
super(message)
this.code = code
@@ -19,20 +20,19 @@ export class APIError extends Error {
}
}
const prefix = 'v0'
export function pathWithPrefix(path: string) {
return `/v0${path}`
return `/${prefix}${path}`
}
export function getWebsocketUrl() {
const endpoint = process.env.NEXT_PUBLIC_API_URL ?? ENV_CONFIG.apiEndpoint
const protocol = endpoint.startsWith('localhost') ? 'ws' : 'wss'
const protocol = BACKEND_DOMAIN.startsWith('localhost') ? 'ws' : 'wss'
return `${protocol}://${endpoint}/ws`
return `${protocol}://${BACKEND_DOMAIN}/ws`
}
export function getApiUrl(path: string) {
const endpoint = process.env.NEXT_PUBLIC_API_URL ?? ENV_CONFIG.apiEndpoint
const protocol = endpoint.startsWith('localhost') ? 'http' : 'https'
const prefix = 'v0'
return `${protocol}://${endpoint}/${prefix}/${path}`
const protocol = BACKEND_DOMAIN.startsWith('localhost') ? 'http' : 'https'
return `${protocol}://${BACKEND_DOMAIN}/${prefix}/${path}`
}

View File

@@ -15,7 +15,13 @@ export function isModId(id: string) {
return MOD_IDS.includes(id)
}
export const DOMAIN = ENV_CONFIG.domain
export const LOCAL_WEB_DOMAIN = 'localhost:3000';
export const LOCAL_BACKEND_DOMAIN = 'localhost:8088';
export const IS_LOCAL = !process.env.VERCEL && !process.env.K_SERVICE;
console.log(IS_LOCAL ? 'Running in local mode' : 'Running in deployed mode', isProd() ? '(prod)' : '(dev)');
export const DOMAIN = IS_LOCAL ? LOCAL_WEB_DOMAIN : ENV_CONFIG.domain
export const BACKEND_DOMAIN = IS_LOCAL ? LOCAL_BACKEND_DOMAIN : ENV_CONFIG.backendDomain
export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig
export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId
@@ -98,7 +104,4 @@ export const RESERVED_PATHS = [
'users',
'web',
'welcome',
]
export const LOCAL_WEB_URL = 'http://localhost:3000';
export const LOCAL_DEV = process.env.GOOGLE_CLOUD_PROJECT == null
]

View File

@@ -2,9 +2,12 @@ import { EnvConfig, PROD_CONFIG } from './prod'
export const DEV_CONFIG: EnvConfig = {
...PROD_CONFIG,
domain: 'dev.compassmeet.com',
backendDomain: 'api.dev.compassmeet.com',
supabaseInstanceId: 'zbspxezubpzxmuxciurg',
supabaseServiceRoleKey: '09wATRREfAzyL5pc', // For database write access (dev). A 16-character password with digits and letters.
supabasePwd: 'FO3y0G7chzdq6aE7', // For database write access (dev). A 16-character password with digits and letters.
supabaseAnonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inpic3B4ZXp1YnB6eG11eGNpdXJnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTc2ODM0MTMsImV4cCI6MjA3MzI1OTQxM30.ZkM7zlawP8Nke0T3KJrqpOQ4DzqPaXTaJXLC2WU8Y7c',
googleApplicationCredentials: 'googleApplicationCredentials-dev.json',
firebaseConfig: {
apiKey: "AIzaSyBspL9glBXWbMsjmtt36dgb2yU0YGGhzKo",
authDomain: "compass-57c3c.firebaseapp.com",
@@ -15,5 +18,5 @@ export const DEV_CONFIG: EnvConfig = {
appId: "1:297460199314:web:c45678c54285910e255b4b",
measurementId: "G-N6LZ64EMJ2",
region: 'us-west1',
}
},
}

View File

@@ -3,9 +3,10 @@ export type EnvConfig = {
firebaseConfig: FirebaseConfig
supabaseInstanceId: string
supabaseAnonKey: string
supabaseServiceRoleKey?: string
supabasePwd?: string
posthogKey: string
apiEndpoint: string
backendDomain: string
googleApplicationCredentials: string
// IDs for v2 cloud functions -- find these by deploying a cloud function and
// examining the URL, https://[name]-[cloudRunId]-[cloudRunRegion].a.run.app
@@ -33,6 +34,11 @@ type FirebaseConfig = {
export const PROD_CONFIG: EnvConfig = {
posthogKey: 'phc_tFvQzHiMVdaAIgE38xqYomMN8q8SB5K45fqmkKNjfBU',
domain: 'compassmeet.com',
backendDomain: 'api.compassmeet.com',
supabaseInstanceId: 'ltzepxnhhnrnvovqblfr',
supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_KEY || '',
supabasePwd: process.env.SUPABASE_DB_PASSWORD || '',
googleApplicationCredentials: process.env.GOOGLE_APPLICATION_CREDENTIALS || '',
firebaseConfig: {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY || '',
authDomain: "compass-130ba.firebaseapp.com",
@@ -46,12 +52,8 @@ export const PROD_CONFIG: EnvConfig = {
},
cloudRunId: 'w3txbmd3ba',
cloudRunRegion: 'uc',
supabaseInstanceId: 'ltzepxnhhnrnvovqblfr',
supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_KEY || '',
apiEndpoint: 'api.compassmeet.com',
adminIds: [
'0vaZsIJk9zLVOWY4gb61gTrRIU73', // Martin
],
faviconPath: '/favicon.ico',
}

View File

@@ -1,6 +1,6 @@
import { readFileSync } from 'fs'
import { SecretManagerServiceClient } from '@google-cloud/secret-manager'
import { zip } from 'lodash'
import {SecretManagerServiceClient} from '@google-cloud/secret-manager'
import {zip} from 'lodash'
import {IS_LOCAL} from "common/envs/constants";
// List of secrets that are available to backend (api, functions, scripts, etc.)
// Edit them at:
@@ -27,6 +27,9 @@ type SecretId = (typeof secrets)[number]
// For deployed google cloud service, no credential is needed.
// For local and Vercel deployments: requires credentials json object.
export const getSecrets = async (credentials?: any, ...ids: SecretId[]) => {
if (!ids.length && IS_LOCAL) return {}
console.log('Fetching secrets...')
let client: SecretManagerServiceClient
if (credentials) {
const projectId = credentials['project_id']
@@ -71,29 +74,3 @@ export const loadSecretsToEnv = async (credentials?: any) => {
}
}
// Get service account credentials from Vercel environment variable or local file.
export const getServiceAccountCredentials = (env: 'PROD' | 'DEV') => {
// Vercel environment variable for service credential.
const value =
env === 'PROD'
? process.env.PROD_FIREBASE_SERVICE_ACCOUNT_KEY
: process.env.DEV_FIREBASE_SERVICE_ACCOUNT_KEY
if (value && !process.env.LOCAL) {
return JSON.parse(value)
}
// Local environment variable for service credential.
const envVar = `GOOGLE_APPLICATION_CREDENTIALS_${env}`
const keyPath = process.env[envVar]
if (keyPath == null) {
throw new Error(
`Please set the ${envVar} environment variable to contain the path to your ${env} environment key file.`
)
}
try {
return JSON.parse(readFileSync(keyPath, { encoding: 'utf8' }))
} catch (e) {
throw new Error(`Failed to load service account key from ${keyPath}.`)
}
}

View File

@@ -1,4 +1,4 @@
import {DOMAIN, LOCAL_DEV, LOCAL_WEB_URL} from 'common/envs/constants'
import {DOMAIN} from 'common/envs/constants'
// opengraph functions that run in static props or client-side, but not in the edge (in image creation)
@@ -10,5 +10,5 @@ export function buildOgUrl<P extends Record<string, string>>(
const generateUrlParams = (params: P) =>
new URLSearchParams(params).toString()
return `https://${domain ?? LOCAL_DEV ? LOCAL_WEB_URL : DOMAIN}/api/og/${endpoint}?` + generateUrlParams(props)
return `https://${domain ?? DOMAIN}/api/og/${endpoint}?` + generateUrlParams(props)
}

View File

@@ -12,14 +12,16 @@
],
"scripts": {
"verify": "yarn --cwd=common verify:dir; yarn --cwd=web verify:dir; yarn --cwd=backend/shared verify:dir",
"lint": "yarn --cwd=web lint-fix; eslint common --fix ; eslint backend/api --fix ; eslint backend/shared --fix", "dev": "./scripts/run_local.sh dev",
"lint": "yarn --cwd=web lint-fix; eslint common --fix ; eslint backend/api --fix ; eslint backend/shared --fix",
"dev": "./scripts/run_local.sh dev",
"prod": "./scripts/run_local.sh prod",
"clean-install": "./scripts/install.sh",
"migrate": "./scripts/migrate.sh",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:update": "jest --updateSnapshot"
"test:update": "jest --updateSnapshot",
"postinstall": "./scripts/post_install.sh"
},
"dependencies": {
"@playwright/test": "^1.54.2",

10
scripts/post_install.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
set -e
cd "$(dirname "$0")"/..
if [ ! -f .env ]; then
cp .env.example .env
echo ".env file created from .env.example"
fi

View File

@@ -4,26 +4,27 @@ set -e
cd "$(dirname "$0")"/..
ENV=${1:-prod}
PROJECT=$2
case $ENV in
dev)
NEXT_ENV=DEV ;;
prod)
NEXT_ENV=PROD ;;
*)
echo "Invalid environment; must be dev or prod."
exit 1
ENVIRONMENT=${1:-dev}
echo "Running in $ENVIRONMENT environment"
case $ENVIRONMENT in
dev)
NEXT_ENV=DEV
;;
prod)
NEXT_ENV=PROD
;;
*)
echo "Unknown environment: $ENVIRONMENT"
exit 1
;;
esac
WEB_DIR=web
source .env
npx dotenv -e .env -- npx concurrently \
-n API,NEXT,TS \
-c white,magenta,cyan \
"cross-env ENV=$NEXT_ENV ENVIRONMENT=$NEXT_ENV yarn --cwd=backend/api dev" \
"cross-env ENVIRONMENT=$NEXT_ENV yarn --cwd=backend/api dev" \
"cross-env NEXT_PUBLIC_FIREBASE_ENV=$NEXT_ENV yarn --cwd=$WEB_DIR serve" \
"cross-env yarn --cwd=$WEB_DIR ts-watch"

View File

@@ -48,6 +48,7 @@ module.exports = {
{ hostname: 'picsum.photos' },
{ hostname: '*.giphy.com' },
{ hostname: 'ui-avatars.com' },
{ hostname: 'localhost' },
],
},
webpack: (config) => {