Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@ tags:
- infrastructure
toc_depth: 3
imgSocial: 'https://zhfonblqamxferhoguzj.supabase.co/functions/v1/generate-og?template=announcement&layout=text-only&copy=When+to+use%0A%5BRead+Replicas%5D%0Avs.+bigger+compute'
imgThumb: 'https://zhfonblqamxferhoguzj.supabase.co/functions/v1/generate-og?template=announcement&layout=text-only&copy=When+to+use%0A%5BRead+Replicas%5D%0Avs.+bigger+compute'
imgThumb: 'https://zhfonblqamxferhoguzj.supabase.co/functions/v1/generate-og?template=ruler&layout=icon-only&icon=icon-database.svg'
---

# When to use Read Replicas vs. bigger compute

When your database starts slowing down, you face a choice: make your existing database bigger, or spread the load across multiple databases. Both approaches work. Neither is universally correct. The right answer depends on your workload, your budget, and where the bottleneck actually is.

This post walks through how to diagnose what is causing your database to slow down, when vertical scaling (bigger compute) makes sense, when horizontal read scaling (Read Replicas) is the better path, and how to make the decision with real numbers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ tags:
- mcp
- integrations
imgSocial: '2026-02-03-supabase-is-now-an-official-claude-connector/og.png'
imgThumb: '2026-02-03-supabase-is-now-an-official-claude-connector/thumb.png'
imgThumb: 'https://zhfonblqamxferhoguzj.supabase.co/functions/v1/generate-og?template=partnerships&layout=icon-only&icon=supabase.svg&icon2=1770136381461-Claude.svg'
toc_depth: 2
---

Expand Down
2 changes: 1 addition & 1 deletion apps/www/_blog/2026-02-06-x-twitter-oauth-2-provider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: 'X / Twitter OAuth 2.0 is now available for Supabase Auth'
description: 'You can now add "Sign in with X" to your application using the new X / Twitter (OAuth 2.0) provider in Supabase Auth.'
author: fady
imgSocial: x-twitter-oauth-2-provider/og.png
imgThumb: x-twitter-oauth-2-provider/og.png
imgThumb: https://zhfonblqamxferhoguzj.supabase.co/functions/v1/generate-og?template=ruler&layout=icon-only&copy=Introducing+Supabase%0Afor+%5BPlatforms%5D&icon=social-x.svg
categories:
- product
tags:
Expand Down
5 changes: 2 additions & 3 deletions apps/www/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { draftMode } from 'next/headers'

import BlogPostClient from './BlogPostClient'
import { SITE_ORIGIN } from '@/lib/constants'
import { getAbsoluteBlogSocialImage } from '@/lib/blog-images'
import { getAllPostSlugs, getPostdata, getSortedPosts } from '@/lib/posts'
import type { Blog, BlogData, PostReturnType } from '@/types/post'

Expand Down Expand Up @@ -40,16 +41,14 @@ export async function generateMetadata({ params }: { params: Promise<Params> }):
}
}

const { isEnabled: isDraft } = await draftMode()
const matter = (await import('gray-matter')).default

// Try to get static markdown post first
try {
const postContent = await getPostdata(slug, '_blog')
const parsedContent = matter(postContent) as unknown as MatterReturn
const blogPost = parsedContent.data
const blogImage = blogPost.imgThumb || blogPost.imgSocial
const metaImageUrl = blogImage && blogImage.startsWith('http') ? blogImage : undefined
const metaImageUrl = getAbsoluteBlogSocialImage(blogPost, SITE_ORIGIN)

return {
title: blogPost.title,
Expand Down
20 changes: 8 additions & 12 deletions apps/www/components/Blog/BlogGridItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import Image from 'next/image'
import Link from 'next/link'

import authors from '@/lib/authors.json'
import {
BLOG_GRID_IMAGE_SIZES,
BLOG_PLACEHOLDER_IMAGE,
getBlogThumbnailImage,
} from '@/lib/blog-images'
import type Author from '@/types/author'
import type PostTypes from '@/types/post'

Expand All @@ -24,15 +29,7 @@ const BlogGridItem = ({ post }: Props) => {
}
}

const resolveImagePath = (img: string | undefined): string | null => {
if (!img) return null
return img.startsWith('/') || img.startsWith('http') ? img : `/images/blog/${img}`
}

const imageUrl =
resolveImagePath(post.imgThumb) ||
resolveImagePath(post.imgSocial) ||
'/images/blog/blog-placeholder.png'
const imageUrl = getBlogThumbnailImage(post) ?? BLOG_PLACEHOLDER_IMAGE

return (
<Link
Expand All @@ -45,8 +42,7 @@ const BlogGridItem = ({ post }: Props) => {
<div className="border-default relative mb-3 w-full aspect-[1.91/1] overflow-hidden rounded-lg border shadow-sm">
<Image
fill
sizes="100%"
quality={100}
sizes={BLOG_GRID_IMAGE_SIZES}
src={imageUrl}
className="scale-100 object-cover overflow-hidden"
alt={`${post.title} thumbnail`}
Expand All @@ -58,7 +54,7 @@ const BlogGridItem = ({ post }: Props) => {
<p>{dayjs(post.date).format('D MMM YYYY')}</p>
{post.readingTime && (
<>
<p></p>
<p>·</p>
<p>{post.readingTime}</p>
</>
)}
Expand Down
16 changes: 7 additions & 9 deletions apps/www/components/Blog/BlogPostRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { ComponentType } from 'react'
import type { PostReturnType, ProcessedBlogData, StaticAuthor, Tag } from 'types/post'
import { Badge } from 'ui'

import { BLOG_POST_HERO_IMAGE_SIZES, getBlogThumbnailImage } from '@/lib/blog-images'
import mdxComponents from '@/lib/mdx/mdxComponents'

const ShareArticleActions = dynamic(() => import('@/components/Blog/ShareArticleActions'))
Expand Down Expand Up @@ -131,11 +132,9 @@ const BlogPostRenderer = ({
</div>
)

const imageUrl = blogMetaData.imgThumb
? blogMetaData.imgThumb.startsWith('/') || blogMetaData.imgThumb.startsWith('http')
? blogMetaData.imgThumb
: `/images/blog/${blogMetaData.imgThumb}`
: ''
const imageUrl = getBlogThumbnailImage(blogMetaData, {
fallbackToPlaceholder: false,
})

return (
<>
Expand Down Expand Up @@ -168,7 +167,7 @@ const BlogPostRenderer = ({
<h1 className="text-2xl sm:text-4xl">{blogMetaData.title}</h1>
<div className="text-light flex space-x-3 text-sm">
<p>{dayjs(blogMetaData.date).format('DD MMM YYYY')}</p>
<p></p>
<p>·</p>
<p>{(blogMetaData as any).readingTime}</p>
</div>
{authors.length > 0 && (
Expand Down Expand Up @@ -231,14 +230,13 @@ const BlogPostRenderer = ({
allowFullScreen={true}
/>
) : (
blogMetaData.imgThumb && (
imageUrl && (
<div className="hidden md:block relative mb-8 w-full aspect-[1.91/1] overflow-auto rounded-lg border">
<Image
src={imageUrl}
alt={blogMetaData.title}
fill
quality={100}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
sizes={BLOG_POST_HERO_IMAGE_SIZES}
className="object-cover m-0"
/>
</div>
Expand Down
20 changes: 8 additions & 12 deletions apps/www/components/Blog/FeaturedThumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import Image from 'next/image'
import Link from 'next/link'

import authors from '@/lib/authors.json'
import {
BLOG_FEATURED_IMAGE_SIZES,
BLOG_PLACEHOLDER_IMAGE,
getBlogThumbnailImage,
} from '@/lib/blog-images'
import type PostTypes from '@/types/post'

function FeaturedThumb(blog: PostTypes) {
Expand All @@ -22,15 +27,7 @@ function FeaturedThumb(blog: PostTypes) {
}

function renderFeaturedThumb(blog: PostTypes, author: any[]) {
const resolveImagePath = (img: string | undefined): string | null => {
if (!img) return null
return img.startsWith('/') || img.startsWith('http') ? img : `/images/blog/${img}`
}

const imageUrl =
resolveImagePath(blog.imgThumb) ||
resolveImagePath(blog.imgSocial) ||
'/images/blog/blog-placeholder.png'
const imageUrl = getBlogThumbnailImage(blog) ?? BLOG_PLACEHOLDER_IMAGE

return (
<div key={blog.slug} className="w-full">
Expand All @@ -42,8 +39,7 @@ function renderFeaturedThumb(blog: PostTypes, author: any[]) {
<Image
src={imageUrl}
fill
sizes="100%"
quality={100}
sizes={BLOG_FEATURED_IMAGE_SIZES}
priority
className="object-cover bg-alternative"
alt="blog thumbnail"
Expand All @@ -52,7 +48,7 @@ function renderFeaturedThumb(blog: PostTypes, author: any[]) {
<div className="flex flex-col space-y-2 lg:col-span-4 xl:justify-center max-w-xl">
<div className="text-lighter flex space-x-2 text-sm">
<span>{blog.formattedDate}</span>
<span></span>
<span>·</span>
<span>{blog.readingTime}</span>
</div>

Expand Down
8 changes: 4 additions & 4 deletions apps/www/components/Events/EventGridItem.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react'
import dayjs from 'dayjs'
// import authors from 'lib/authors.json'
import Image from 'next/image'
import Link from 'next/link'
import React from 'react'

// import type Author from '~/types/author'
import type PostTypes from '../../types/post'

import dayjs from 'dayjs'

interface Props {
event: PostTypes
}
Expand Down Expand Up @@ -58,7 +58,7 @@ const EventGridItem = ({ event }: Props) => {
<p>{dayjs(event.date).format('D MMM YYYY')}</p>
{event.readingTime && (
<>
<p></p>
<p>·</p>
<p>{event.readingTime}</p>
</>
)}
Expand Down
12 changes: 3 additions & 9 deletions apps/www/layouts/comparison.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { MDXRemote } from 'next-mdx-remote'
import { NextSeo } from 'next-seo'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import React from 'react'
import CTABanner from '~/components/CTABanner'
import DefaultLayout from '~/components/Layouts/Default'
import { getAbsoluteBlogSocialImage } from '~/lib/blog-images'
import { generateReadingTime } from '~/lib/helpers'

interface Props {
Expand All @@ -31,8 +31,6 @@ const LayoutComparison = ({ components, props }: Props) => {
)
}

const { basePath } = useRouter()

const NextCard = (props: any) => {
const { post, label, className } = props
return (
Expand Down Expand Up @@ -77,12 +75,8 @@ const LayoutComparison = ({ components, props }: Props) => {
}),
},
images: (() => {
const img = props.blog.imgSocial || props.blog.imgThumb
if (!img) return []
const url =
img.startsWith('/') || img.startsWith('http')
? img
: `https://supabase.com${basePath}/images/blog/${img}`
const url = getAbsoluteBlogSocialImage(props.blog, 'https://supabase.com')
if (!url) return []
return [{ url }]
})(),
}}
Expand Down
91 changes: 91 additions & 0 deletions apps/www/lib/blog-images.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, expect, it, vi } from 'vitest'

import {
BLOG_PLACEHOLDER_IMAGE,
getAbsoluteBlogSocialImage,
getBlogSocialImage,
getBlogThumbnailImage,
resolveBlogImagePath,
validateBlogFrontmatterImages,
} from './blog-images'

describe('blog image helpers', () => {
it('prefers imgThumb for on-site blog thumbnails', () => {
expect(
getBlogThumbnailImage({
imgThumb: 'example/thumb.png',
imgSocial: 'example/og.png',
})
).toBe('/images/blog/example/thumb.png')
})

it('falls back to imgSocial for on-site blog thumbnails', () => {
expect(
getBlogThumbnailImage({
imgSocial: 'example/og.png',
})
).toBe('/images/blog/example/og.png')
})

it('prefers imgSocial for social metadata', () => {
expect(
getBlogSocialImage({
imgThumb: 'example/thumb.png',
imgSocial: 'example/og.png',
})
).toBe('/images/blog/example/og.png')
})

it('returns absolute URLs unchanged and prefixes relative paths', () => {
expect(resolveBlogImagePath('https://example.com/og.png')).toBe('https://example.com/og.png')
expect(resolveBlogImagePath('/images/blog/example/og.png')).toBe('/images/blog/example/og.png')
expect(resolveBlogImagePath('example/og.png')).toBe('/images/blog/example/og.png')
})

it('builds absolute social image URLs', () => {
expect(
getAbsoluteBlogSocialImage(
{
imgSocial: 'example/og.png',
},
'https://supabase.com'
)
).toBe('https://supabase.com/images/blog/example/og.png')
})

it('uses the placeholder when neither image is present', () => {
expect(getBlogThumbnailImage({})).toBe(BLOG_PLACEHOLDER_IMAGE)
expect(getBlogThumbnailImage({}, { fallbackToPlaceholder: false })).toBeUndefined()
})

it('warns when blog frontmatter is missing one of the image fields', () => {
const warn = vi.fn()

validateBlogFrontmatterImages(
{
imgThumb: 'example/thumb.png',
},
'/tmp/example-post.mdx',
warn
)

expect(warn).toHaveBeenCalledWith(expect.stringContaining('missing "imgSocial"'))
})

it('warns when a blog image uses the prefixed public path', () => {
const warn = vi.fn()

validateBlogFrontmatterImages(
{
imgThumb: '/images/blog/example/thumb.png',
imgSocial: 'example/og.png',
},
'/tmp/example-prefixed-post.mdx',
warn
)

expect(warn).toHaveBeenCalledWith(
expect.stringContaining('should not include the "/images/blog/" prefix')
)
})
})
Loading
Loading