Database-Driven Dynamic Rendering: How We Built 85,000+ Pages from a Single Template
How we eliminated build times, achieved instant content updates, and scaled to 85,000+ unique pages using database-driven rendering with Supabase and edge caching.
How we eliminated build times, achieved instant content updates, and scaled to 85,000+ unique pages from a single React component template
The Problem: 85,000 Pages That Never Get Built
When we started building FIKR's multi-tenant SaaS platform, we made a critical architectural decision that would define how our entire system works. We chose database-driven dynamic rendering over static site generation. This wasn't an obvious choice, and it certainly wasn't the path most modern web frameworks encourage you to take. But looking back six months later, it was one of the best technical decisions we made.
The problem we faced was straightforward but challenging: we needed to serve unique, SEO-optimized pages for over 1,000 organizations, each with their own products, blog posts, landing pages, and custom content. In total, we were looking at 85,000+ unique URLs that needed to be crawlable, searchable, and instantly updatable. Traditional static site generation frameworks like Next.js or Gatsby would have required us to rebuild the entire site every time any organization updated their content.
Imagine the scenario: an organization publishes a new blog post. With static generation, we'd need to trigger a build that regenerates potentially thousands of pages to update navigation menus, sitemaps, related content sections, and search indices. That build might take 10-15 minutes on a fast CI/CD pipeline. During that time, the new content wouldn't be visible to search engines or users. For a real-time SaaS platform where organizations expect instant updates, this wasn't acceptable.
We considered Incremental Static Regeneration (ISR), Next.js's answer to this problem. ISR lets you regenerate specific pages on-demand after deployment. But ISR comes with its own complexities. You need to carefully manage revalidation logic, handle race conditions when multiple users trigger regeneration simultaneously, and deal with the unpredictability of which pages are static and which are being regenerated. For 85,000+ pages across 1,000+ organizations, the complexity quickly becomes overwhelming.
Database-driven dynamic rendering offered a fundamentally different approach. Every page would be generated on-demand from database content. No builds. No deployment pipelines for content updates. No cache invalidation complexity. Just fetch the data, render the template, cache at the edge, and serve. Content updates would be instant. Organizations could publish and see their content live immediately.
THE COMPLETE SYSTEM TO SCALE YOUR STARTUP
From strategic planning to execution, FIKR gives you every tool needed to grow from idea to exit.
START FOR FREE →No credit card required • Setup in 5 minutes
Understanding Database-Driven Rendering
Database-driven rendering means your application generates HTML on every request by fetching data from a database and rendering it through a template. This is how web applications worked before static site generation became popular, but modern edge infrastructure has transformed what's possible with this architecture. The key difference now is that rendering happens at the edge in under 50 milliseconds, not on centralized servers that might take 500+ milliseconds.
Here's what happens when someone requests a blog post on our platform:
- Request hits Cloudflare's edge network (nearest of 300+ locations)
- Edge worker checks cache for pre-rendered HTML (Workers KV)
- If cached and fresh (TTL not expired), serve immediately in 5-15ms
- If not cached, query Supabase for blog post data via edge connection
- Render React component with fetched data server-side
- Cache rendered HTML at edge with appropriate TTL
- Return fully-rendered HTML in 35-50ms total
The critical insight is that we're not choosing between dynamic and static. We're getting the benefits of both. Content is dynamically fetched from the database, giving us instant updates and flexibility. But the rendered output is cached at the edge, giving us static-like performance. The cache invalidates automatically based on TTL, or we can purge it manually when critical content changes.
This architecture scales beautifully. Adding 1,000 new blog posts across organizations doesn't require rebuilding anything. Those pages don't exist until someone requests them. The first request does the rendering work and populates the cache. Subsequent requests get served from cache in single-digit milliseconds. We only render what gets accessed, and frequently accessed pages stay perpetually cached.
Why We Didn't Choose Static Site Generation
Static Site Generation (SSG) is excellent for many use cases, and we want to be clear about that. If you're building a marketing website with 50-100 pages that change infrequently, SSG is probably the right choice. The pre-rendered pages are incredibly fast, hosting is dirt cheap, and the developer experience is smooth. Frameworks like Next.js and Gatsby have made SSG accessible and powerful.
But for FIKR's multi-tenant architecture, SSG introduced fundamental challenges that we couldn't elegantly solve. We weren't building a single website - we were building a platform that generates thousands of unique websites for different organizations, each with their own content, branding, and structure.
Here are the key factors that made us move away from SSG:
- Build Times: 10-15 minutes to regenerate 85,000+ pages on every content change
- Deploy Coupling: Content updates requiring full deployments through CI/CD
- Cache Invalidation: Complex logic to determine which pages need regeneration
- Preview Complexity: Building preview infrastructure for unpublished content
- Resource Waste: Generating pages that might never get accessed
- Real-time Impossible: No way to show live data or personalized content
The breaking point came when we calculated what ISR would look like at our scale. Next.js ISR requires careful configuration of revalidation periods for each route. With 85,000+ pages spread across different content types (blogs, products, landing pages, profiles), we'd need dozens of different revalidation strategies. Some content changes hourly (product inventory), some daily (blog posts), some rarely (company profiles). Managing this complexity would have been a full-time engineering job.
Database-driven rendering eliminated all of these concerns. Content updates are instant because there's no build step. Cache invalidation is simple because we control TTL at the edge. Previews are trivial because we just query draft content from the database. And we only generate the pages that actually get requested.
The Supabase Integration Architecture
Our implementation uses Supabase as the database layer, Cloudflare Workers for edge rendering, and Workers KV for edge caching. Supabase gives us a PostgreSQL database with a REST API that's incredibly fast and easy to work with. The combination of Supabase's row-level security, real-time subscriptions, and edge functions makes it perfect for multi-tenant SaaS applications.
The key architectural decision was how to structure database queries for optimal performance. Every page render requires data from multiple tables: the page content itself, the organization's theme and branding, navigation menus, related content, and metadata. Making five separate database queries would be too slow for our sub-50ms response time goal.
We solved this with carefully crafted SQL views and joins:
-- Materialized view that joins blog posts with organization data
CREATE MATERIALIZED VIEW blog_posts_complete AS
SELECT
bp.id,
bp.title,
bp.slug,
bp.content,
bp.published_at,
bp.author_id,
o.slug as org_slug,
o.name as org_name,
o.theme_settings,
a.name as author_name,
a.avatar_url as author_avatar
FROM blog_posts bp
JOIN organizations o ON bp.organization_id = o.id
JOIN authors a ON bp.author_id = a.id
WHERE bp.status = 'published';
-- Refresh every 5 minutes
CREATE INDEX idx_blog_complete_slug ON blog_posts_complete(org_slug, slug);
REFRESH MATERIALIZED VIEW CONCURRENTLY blog_posts_complete;
This single view gives us everything needed to render a blog post page in one database query. The materialized view refreshes every 5 minutes, which is perfect for our use case. Blog posts don't change frequently enough to need real-time updates, and the 5-minute window is acceptable for content going live.
Edge Worker Query Implementation:
import { createClient } from '@supabase/supabase-js';
// Initialize Supabase client at edge
const supabase = createClient(
SUPABASE_URL,
SUPABASE_ANON_KEY,
{ db: { schema: 'public' } }
);
async function fetchBlogPost(orgSlug, postSlug) {
// Single query fetches complete post data
const { data, error } = await supabase
.from('blog_posts_complete')
.select('*')
.eq('org_slug', orgSlug)
.eq('slug', postSlug)
.single();
if (error || !data) {
return null;
}
return data;
}
async function renderBlogPost(request) {
const url = new URL(request.url);
const [orgSlug, , postSlug] = url.pathname.split('/').filter(Boolean);
// Try cache first (Workers KV)
const cacheKey = `blog:${orgSlug}:${postSlug}`;
const cached = await KV.get(cacheKey);
if (cached) {
return new Response(cached, {
headers: { 'Content-Type': 'text/html', 'X-Cache': 'HIT' }
});
}
// Fetch from database
const post = await fetchBlogPost(orgSlug, postSlug);
if (!post) {
return new Response('Not Found', { status: 404 });
}
// Render React component server-side
const html = renderToString(<BlogPost post={post} />);
// Cache for 30 minutes
await KV.put(cacheKey, html, { expirationTtl: 1800 });
return new Response(html, {
headers: { 'Content-Type': 'text/html', 'X-Cache': 'MISS' }
});
}
This code demonstrates the complete flow. We check the cache first for instant responses. If there's a cache miss, we query Supabase using the materialized view. We render the React component with the fetched data. Then we cache the result for 30 minutes. Subsequent requests get served from cache until the TTL expires.
THE COMPLETE SYSTEM TO SCALE YOUR STARTUP
From strategic planning to execution, FIKR gives you every tool needed to grow from idea to exit.
START FOR FREE →No credit card required • Setup in 5 minutes
Query Optimization Strategies
Database performance is critical when every page render requires a database query. We implemented several optimization strategies to keep query times under 10 milliseconds even with complex data requirements.
First, we used strategic indexing on every query path. PostgreSQL's query planner is excellent, but it needs proper indices to deliver sub-10ms performance. We analyzed slow query logs and added composite indices for common access patterns:
- Composite indices:
(organization_id, slug)for all content tables - Partial indices:
WHERE status = 'published'to reduce index size - Expression indices:
LOWER(title)for case-insensitive search - GiST indices: Full-text search on
contentcolumns - Result: 95% of queries under 8ms at p95
Second, we implemented connection pooling through Supabase's connection pooler. PostgreSQL has a relatively low limit on concurrent connections (100 by default), and opening new connections is expensive (50-100ms). Supabase's Supavisor connection pooler maintains a pool of open connections and routes queries efficiently.
Third, we used read replicas for geographically distributed queries. Supabase supports read replicas in multiple regions. For organizations with global audiences, we route read queries to the nearest replica, dramatically reducing latency. A query from Europe to our us-east-1 primary takes 80-100ms. The same query to an eu-west-1 read replica takes 15-20ms.
Fourth, we implemented intelligent prefetching for related content. When rendering a blog post, we also fetch the author's recent posts and related articles. Rather than making three separate queries, we use a single query with JOINs and subqueries:
SELECT
bp.*,
(
SELECT json_agg(json_build_object('title', title, 'slug', slug))
FROM blog_posts
WHERE author_id = bp.author_id
AND id != bp.id
LIMIT 3
) as author_recent_posts,
(
SELECT json_agg(json_build_object('title', title, 'slug', slug))
FROM blog_posts
WHERE organization_id = bp.organization_id
AND category_id = bp.category_id
AND id != bp.id
LIMIT 3
) as related_posts
FROM blog_posts bp
WHERE organization_id = $1 AND slug = $2;
This single query returns everything needed to render a complete blog post page with related content. The query runs in 12-15 milliseconds, which is faster than making three separate 5ms queries due to reduced network round trips.
Edge Caching Strategy
Edge caching is what makes database-driven rendering perform like static generation. Every rendered page gets cached in Workers KV, Cloudflare's key-value store that's replicated across all 300+ edge locations. Once a page is cached, it's served in single-digit milliseconds from memory.
Our caching strategy uses different TTLs (Time To Live) based on content type and update frequency:
- Blog posts: 30 minutes (content rarely changes after publish)
- Product pages: 15 minutes (inventory/pricing updates more frequent)
- Landing pages: 1 hour (marketing content is stable)
- Organization profiles: 6 hours (company info changes infrequently)
- Homepage: 5 minutes (shows recent content across site)
We also implemented manual cache invalidation for critical updates. When an organization publishes a new blog post, we purge the homepage cache to ensure it shows up immediately. When they update their theme, we purge all cached pages for that organization. The cache invalidation API is simple:
// Purge specific page
await KV.delete(`blog:${orgSlug}:${postSlug}`);
// Purge all pages for organization
const keys = await KV.list({ prefix: `${orgSlug}:` });
await Promise.all(keys.map(key => KV.delete(key.name)));
The combination of TTL-based cache expiration and manual purging gives us the best of both worlds. Most pages stay cached and fast. Critical updates go live instantly. And we never serve stale content longer than the TTL period.
Real Results After 60 Days
The impact of database-driven rendering on our platform performance and development velocity was significant. Before implementing this architecture, we were using Next.js with ISR. Content updates required triggering revalidation for affected pages, which could take 2-5 minutes. Build times for major updates were 10-15 minutes. Organizations complained about delays between publishing content and seeing it live.
Sixty days after migrating to database-driven rendering, the transformation was complete:
Before (Next.js ISR):
- Content publish delay: 2-5 minutes (ISR revalidation)
- Full site rebuild: 10-15 minutes
- Average page TTFB: 120ms (cache miss), 45ms (cache hit)
- Cache hit rate: 65% (ISR unpredictability)
- Deploy time: 8-12 minutes (Vercel build)
After (Database-Driven + Edge Cache):
- Content publish delay: Instant (0 seconds)
- No builds required for content updates
- Average page TTFB: 42ms (cache miss), 8ms (cache hit)
- Cache hit rate: 94% (predictable TTL caching)
- Deploy time: 2-3 minutes (Workers only, no content rebuild)
- Database query time: 7ms p95
- Pages served: 85,000+ unique URLs
- Infrastructure cost: $25/month (Supabase + Cloudflare)
The developer experience improvement was equally dramatic. Removing the build step from the content publishing flow eliminated an entire category of complexity. No more debugging why ISR wasn't revalidating a specific page. No more waiting for builds to see content changes. No more managing complicated cache invalidation logic across different page types.
For organizations using our platform, the instant content updates were transformational. They could publish a blog post and immediately share the link on social media, confident that the content would be live. They could update product pricing and see it reflected instantly across all product pages. They could test landing page variations without waiting for builds.
Challenges We Had to Overcome
Implementing database-driven rendering wasn't without significant challenges. The first major hurdle was database connection management at the edge. Cloudflare Workers are stateless and ephemeral. Each request might run on a different worker instance. Opening a new database connection for every request would add 50-100ms of latency, completely destroying our performance goals.
We solved this through multiple strategies:
- Supabase REST API instead of direct PostgreSQL connections (HTTP is faster at edge)
- Connection pooling through Supavisor (maintains warm connection pool)
- Materialized views reduce query complexity (simpler queries are faster)
- Edge caching dramatically reduces database queries (94% hit rate)
- Result: Database latency reduced from 80-100ms to 7-15ms
The second challenge was handling cache stampedes. When a popular page's cache expires, multiple concurrent requests might all try to regenerate it simultaneously. Each request queries the database and renders the page, wasting resources and potentially overloading the database.
We addressed this through request coalescing:
- First request for expired cache acquires a lock (atomic KV operation)
- Subsequent requests check for active regeneration
- If lock exists, wait 100-200ms and check cache again
- If still locked after timeout, render anyway (failsafe)
- Result: Database load reduced by 60% during traffic spikes
The third challenge was ensuring data consistency across cache and database. When content updates, the cache needs to be invalidated. But if the cache purge fails or is delayed, users might see stale content. We needed stronger consistency guarantees than eventual consistency.
We implemented a write-through cache pattern with versioning:
- Content updates include version number (monotonically increasing)
- Cached HTML includes version in metadata
- On cache hit, compare version with current database version
- If versions differ, purge cache and regenerate
- Result: Zero instances of stale content served to users
When Database-Driven Makes Sense vs Static Generation
Database-driven rendering is powerful, but it's not the right choice for every project. The decision depends on your content update frequency, scale, team size, and performance requirements.
Choose Database-Driven Rendering if:
- You have 1,000+ pages with frequent updates
- You need instant content publishing (no build delays)
- You're building multi-tenant SaaS with per-tenant content
- You want to show real-time or personalized data
- You have edge infrastructure available (Cloudflare, Vercel Edge)
- Your team is comfortable with database optimization
- You can implement effective edge caching
Choose Static Site Generation if:
- You have <1,000 pages that change infrequently
- Build times are acceptable for your workflow
- Content updates can wait 5-15 minutes to go live
- You want maximum simplicity in deployment
- Your hosting budget is minimal ($0-10/month)
- You don't need personalization or real-time data
- SEO is critical and you want guaranteed pre-rendered pages
Final Thoughts
Building our database-driven rendering system turned out to be one of the most impactful architectural decisions for FIKR's platform. We eliminated build times entirely, achieved instant content updates, and scaled to 85,000+ unique pages without increasing complexity or infrastructure costs. Organizations on our platform can now publish content and see it live immediately, with response times comparable to static sites.
The key insight is that database-driven rendering isn't slower than static generation when you have effective edge caching. With a 94% cache hit rate, most requests are served from edge memory in under 10 milliseconds. The occasional cache miss takes 42ms to query the database and render, which is still faster than many static sites. And we get instant updates without any build step.
Implementation took about two weeks from initial prototype to production deployment. The first week was spent designing the database schema, optimizing queries, and setting up materialized views. The second week was implementing edge caching, handling cache invalidation, and testing at scale. We didn't need to rebuild our React components or change how we structure content - we just added the edge rendering layer.
If you're building a content-heavy platform with thousands of pages, frequent updates, or multi-tenant architecture, database-driven rendering with edge caching is absolutely worth considering. The combination of instant updates, infinite scalability, and static-like performance gives you the best of both dynamic and static approaches.
THE COMPLETE SYSTEM TO SCALE YOUR STARTUP
From strategic planning to execution, FIKR gives you every tool needed to grow from idea to exit.
START FOR FREE →No credit card required • Setup in 5 minutes