Skip to content

chore: static assets cdn attempt 2 #160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/web/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
Expand Down
31 changes: 31 additions & 0 deletions apps/web/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import webpack from 'webpack'

/** @type {import('next').NextConfig} */
const nextConfig = {
assetPrefix: getAssetPrefix(),
reactStrictMode: false,
env: {
NEXT_PUBLIC_PGLITE_VERSION: await getPackageVersion('@electric-sql/pglite'),
Expand Down Expand Up @@ -66,6 +67,19 @@ const nextConfig = {

return redirects
},
headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: "worker-src 'self' https://frontend-assets.supabase.com",
},
],
},
]
},
}

export default nextConfig
Expand All @@ -85,3 +99,20 @@ async function getPackageVersion(module) {
const packageJson = await getPackageJson(module)
return packageJson.version
}

function getAssetPrefix() {
// If not force enabled, but not production env, disable CDN
if (process.env.FORCE_ASSET_CDN !== '1' && process.env.VERCEL_ENV !== 'production') {
return undefined
}

// Force disable CDN
if (process.env.FORCE_ASSET_CDN === '-1') {
return undefined
}

// @ts-ignore
return `https://frontend-assets.supabase.com/${
process.env.SITE_NAME
}/${process.env.VERCEL_GIT_COMMIT_SHA.substring(0, 12)}`
}
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "npm run build:sw && next dev",
"build": "npm run build:sw && next build",
"build": "npm run build:sw && next build && ./../../scripts/upload-static-assets.sh",
"build:sw": "vite build",
"start": "next start",
"lint": "next lint",
Expand Down
94 changes: 94 additions & 0 deletions scripts/upload-static-assets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/bin/bash

#######

# This script is used to upload static build assets (JS, CSS, ...) and public static files (public folder) to a CDN.
# We're using Cloudflare R2 as CDN.
# By using a CDN, we can serve static assets extremely fast while saving big time on egress costs.
# An alternative is proxying via CF, but that comes with Orange-To-Orange issues (Cloudflare having issues with Cloudflare) and increased latency as there is a double TLS termination.
# The script is only supposed to run on production deployments and is not run on any previews.

# By using a dynamic path including the env, app and commit hash, we can ensure that there are no conflicts.
# Static assets from previous deployments stick around for a while to ensure there are no "downtimes".

# Advantages of the CDN approach we're using:

# Get rid of egress costs for static assets across our apps on Vercel
# Disable CF proxying and get around these odd timeouts issues
# Save ~20ms or so for asset requests, as there is no additional CF proxying and we avoid terminating SSL twice
# Always hits the CDN, gonna be super quick
# Does not run on local or preview environments, only on staging/prod deployments
# There are no other disadvantages - you don't have to consider it when developing locally, previews still work, everything on Vercel works as we're used to

#######

# If asset CDN is specifically disabled (i.e. studio self-hosted), we skip
if [[ "$FORCE_ASSET_CDN" == "-1" ]]; then
echo "Skipping asset upload. Set FORCE_ASSET_CDN=1 or VERCEL_ENV=production to execute."
exit 0
fi

# Check for force env var or production environment
if [[ "$FORCE_ASSET_CDN" != "1" ]] && [[ "$VERCEL_ENV" != "production" ]]; then
echo "Skipping asset upload. Set FORCE_ASSET_CDN=1 or VERCEL_ENV=production to execute."
exit 0
fi

# Set the cdnBucket variable based on NEXT_PUBLIC_ENVIRONMENT
if [[ "$NEXT_PUBLIC_ENVIRONMENT" == "staging" ]]; then
BUCKET_NAME="frontend-assets-staging"
else
BUCKET_NAME="frontend-assets-prod"
fi

STATIC_DIR=".next/static"
PUBLIC_DIR="public"

# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Install AWS CLI if not present
if ! command -v aws &> /dev/null; then
echo -e "${YELLOW}Setting up AWS CLI...${NC}"
curl -s "https://awscli.amazonaws.com/awscli-exe-linux-x86_64-2.22.35.zip" -o "awscliv2.zip"
unzip -q awscliv2.zip
export PATH=$PWD/aws/dist:$PATH
rm awscliv2.zip
fi

# Check if directory exists
if [ ! -d "$STATIC_DIR" ]; then
echo -e "${YELLOW}Directory $STATIC_DIR not found!${NC}"
echo "Make sure you're running this script from your Next.js project root."
exit 1
fi

# Upload files with cache configuration and custom endpoint
echo -e "${YELLOW}Uploading static files to R2...${NC}"
aws s3 sync "$STATIC_DIR" "s3://$BUCKET_NAME/$SITE_NAME/${VERCEL_GIT_COMMIT_SHA:0:12}/_next/static" \
--endpoint-url "$ASSET_CDN_S3_ENDPOINT" \
--cache-control "public,max-age=604800,immutable" \
--region auto \
--only-show-errors

# Some public files may be referenced through CSS (relative path) and therefore they would be requested via the CDN url
# To ensure we don't run into some nasty debugging issues, we upload the public files to the CDN as well
echo -e "${YELLOW}Uploading public files to R2...${NC}"
aws s3 sync "$PUBLIC_DIR" "s3://$BUCKET_NAME/$SITE_NAME/${VERCEL_GIT_COMMIT_SHA:0:12}" \
--endpoint-url "$ASSET_CDN_S3_ENDPOINT" \
--cache-control "public,max-age=604800,immutable" \
--region auto \
--only-show-errors

if [ $? -eq 0 ]; then
echo -e "${GREEN}Upload completed successfully!${NC}"

# Clean up local static files so we prevent a double upload
echo -e "${YELLOW}Cleaning up local static files...${NC}"
rm -rf "$STATIC_DIR"/*
echo -e "${GREEN}Local static files cleaned up${NC}"

# We still keep the public dir, as Next.js does not officially support serving the public files via CDN
fi
7 changes: 6 additions & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@
"KV_*",
"SUPABASE_*",
"LOGFLARE_*",
"REDIRECT_LEGACY_DOMAIN"
"REDIRECT_LEGACY_DOMAIN",
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"FORCE_ASSET_CDN",
"ASSET_CDN_S3_ENDPOINT",
"SITE_NAME"
],
"outputs": [".next/**", "!.next/cache/**"],
"cache": true
Expand Down