An enterprise-grade membership platform powered by modern technologies, edge computing, and AI-assisted development.
This post explains how to build a productizable membership platform with Astro 5, Paddle, and Cloudflare Pages. I’m presenting an architecture that combines Astro’s static site generation power, Paddle’s subscription management, and Cloudflare’s edge computing infrastructure to optimize performance, security, and developer experience.
My goal is to transform this system—which I develop solo and manage alongside Claude Code during development, and Qwen via LM Studio locally for migrating content from Grav CMS to Astro—into a reusable template for developers with similar needs. Beyond traditional blogs or CMS platforms, it features enterprise capabilities like tier-based access control, premium content gating, and automated webhook processing.
Why This Architecture?
When building a modern content platform, you face three fundamental challenges:
- Performance vs Dynamic Content: Static sites are fast, but membership systems require dynamic functionality
- Payment Integration: Subscription management, webhooks, invoicing, and tax complexity
- Global Distribution: Low latency, CDN, and edge computing requirements
This architecture solves these problems with a hybrid approach:
- Static pages are generated at build-time with Astro (speed)
- Dynamic APIs run at the edge via Cloudflare Pages Functions (flexibility)
- Subscription logic is delegated to Paddle (complexity externalized)
- Database is managed with Supabase (real-time, secure)
Technology Stack
Frontend & Build
| Technology | Version | Purpose |
|---|---|---|
| Astro | 5.15.2 | Static Site Generator + Edge Functions |
| TypeScript | 5.9.3 | Type-safe development |
| TailwindCSS | 3.4.17 | Utility-first CSS |
| MDX | 4.3.9 | Markdown + JSX components |
| Pagefind | 1.4.0 | Client-side search indexing |
Backend & Services
| Technology | Purpose |
|---|---|
| Supabase | PostgreSQL database, authentication, real-time |
| Paddle | Subscription management, payments, invoicing |
| Cloudflare Pages | Hosting, edge functions, global CDN |
| Cloudflare KV | Key-value cache (search, premium content) |
| Cloudflare R2 | Object storage (images, assets) |
Project Structure
ceaksan-v4.0/
├── src/
│ ├── components/ # 68+ Astro components
│ │ ├── pages/ # Page-specific components
│ │ ├── ui/ # UI component library
│ │ └── sections/ # Reusable sections
│ ├── content/ # Astro Content Collections
│ │ ├── posts/ # Blog posts (MDX)
│ │ └── courses/ # Video courses
│ ├── lib/ # 52 library modules
│ │ ├── paddle/ # Paddle integration
│ │ ├── supabase.ts # Server-side client
│ │ └── supabase-browser.js # Browser client
│ ├── pages/ # File-based routing
│ └── styles/ # Global styles
├── functions/ # Cloudflare Pages Functions
│ └── api/ # 15 API endpoints
├── .claude/ # Claude Code integration
│ ├── commands/ # Slash commands
│ ├── subagents/ # Specialized agents
│ └── hooks/ # Automatic hooks
└── public/ # Static assets
Astro Configuration
When working with Astro 5, there’s a critical rule: Never use output: 'hybrid'—this mode doesn’t exist in Astro 5.
The Cloudflare Free Tier Challenge
A key decision in this project was not using Cloudflare’s official Astro adapter. Why?
Cloudflare Pages’ free tier has specific build limits. When using the @astrojs/cloudflare adapter, the dependencies and build process required by the adapter pushed against these limits. For more details, see Cloudflare’s Astro deployment guide.
Solution without the adapter:
// astro.config.mjs
export default defineConfig({
output: 'static', // CRITICAL: Always 'static'
i18n: {
defaultLocale: 'tr',
locales: ['tr', 'en'],
routing: {
prefixDefaultLocale: true // /tr/page and /en/page
}
}
// NOTE: Cloudflare adapter NOT used
});
Pros and Cons of This Approach
Pros:
- Staying within Cloudflare free tier limits
- Smaller build output
- Faster build times
- Fewer dependencies
Cons:
- Cloudflare-specific features (Image CDN, etc.) not directly usable
- Manual
functions/directory management for server-side rendering - Missing automatic optimizations the adapter would provide
Dynamic Pages
For dynamic pages, Cloudflare Pages Functions are used in the functions/ directory:
// functions/api/example.ts
export const onRequest: PagesFunction = async (context) => {
// KV, R2, D1 access available here
return new Response(JSON.stringify({ data }));
};
For Astro pages, prerender can be disabled on a per-page basis:
// Dynamic page example
export const prerender = false;
Benefits of this approach:
- Static pages are generated at build-time (fast, cheap)
- Dynamic pages run at the edge (KV, R2 access)
- Bundle size is optimized (no unnecessary SSR code)
- Free tier limits are respected
Paddle Subscription Integration
Tier System
The platform offers a three-tier membership system:
| Tier | Access |
|---|---|
| Insider | Content |
| Maker | Content + Files + Code |
| Master | Everything + Courses |
Critical Rule: Course access is only available with Master tier. This is validated via price_id—not tier name. Because tier names may change over time, but Paddle’s price_id remains consistent.
Webhook Processing
Paddle webhooks are processed at the /api/paddle/webhook endpoint:
// Webhook event types
- subscription.created // New subscription
- subscription.updated // Plan change
- subscription.canceled // Cancellation
- transaction.completed // Payment confirmation
Each webhook event:
- Signature verification is performed
- Logged to
webhook_eventstable subscriptionstable is updatedevent_idis checked for idempotency
Subscription Update Logic
// Upgrade: Immediate, prorated
proration_billing_mode: 'prorated_immediately'
// Downgrade: Next billing period
proration_billing_mode: 'prorated_next_billing_period'
// Cancellation: Access continues until period end
effective_from: 'next_billing_period'
Supabase Database Architecture
Supabase forms the core database infrastructure of the platform:
- Customer management: User information and Paddle integration
- Subscription tracking: Subscription status, plan info, billing
- Course enrollments: Enrollment management and access control
Every table is secured with Row Level Security (RLS). Users can only access their own data.
Access Control Flow
1. User wants to access premium content
2. Auth check → Supabase session
3. Tier validation → price_id based check
4. Result: Show content or paywall
Cloudflare Integration
KV Namespaces (6 total)
| Namespace | Usage |
|---|---|
| PREMIUM_CONTENT | Premium post content |
| COURSE_CONTENT | Course content |
| ANALYTICS | Usage metrics |
| FEATURE_FLAGS | Feature flags |
| RATE_LIMITER | Rate limiting |
| SESSION | Session management |
R2 Object Storage
Bucket: your-assets
URL: https://assets.your-domain.com
Usage: Images, files, media
API Endpoints
The platform includes APIs running on Cloudflare Pages Functions:
- Paddle integration: Webhook processing, subscription management
- Content access: Premium content and course delivery
- Pricing: Dynamic price information
Search with Pagefind
Pagefind was chosen over Algolia or ElasticSearch:
- Client-side search: No server costs
- Build-time indexing: Automatic
- Multilingual support: Separate TR and EN indexes
- Small bundle: ~100KB
// Automatic indexing after build
// Created in dist/pagefind/ directory
// Search usage
const pagefind = await import('/pagefind/pagefind.js');
await pagefind.init();
const results = await pagefind.search('astro');
With the latest update, Pagefind search is connected to the header search button. When users click the search button, the Pagefind modal opens and language-based filtering is automatically applied.
Premium Content Gating
Tier Definition via Frontmatter
---
title: "Premium Content"
requiredTier: ['insider'] # Array format
member: true
---
Client-Side Access Check
import { checkContentAccess } from '@/lib/content-access-client.js';
const hasAccess = await checkContentAccess(getSupabaseClient, {
requiredTier: ['insider'],
onAccessGranted: () => showContent(),
onAccessDenied: () => showPaywall()
});
Premium Content Delivery
Premium content is not rendered client-side. It’s fetched via API:
// /api/content/premium-post
// 1. Auth check
// 2. Tier validation
// 3. Fetch content from KV
// 4. Return rendered HTML
i18n (Multilingual Support)
URL-Based Routing
/tr/contents/ → Turkish content
/en/contents/ → English content
Language Detection
const lang = window.location.pathname.startsWith('/en') ? 'en' : 'tr';
const text = lang === 'tr' ? 'Turkish Text' : 'English Text';
Content Collections
src/content/posts/2026/01/01.slug/
├── tr.mdx # Turkish version
└── en.mdx # English version
Claude Code Integration
Verification-First Approach
Give Claude a way to verify its work — Boris Mann
This principle forms the foundation of developing with Claude Code. Every change must be verifiable.
Slash Commands
Slash commands defined for the project:
/test-and-build - Run tests, build
/verify-changes - Comprehensive verification
/commit-push-pr - Commit + Push + Create PR
/preview-deploy - Deploy to preview environment
/kv - KV management
Custom Skills
Skills defined in .claude/commands/:
- test-and-build.md: Build and test automation
- verify-changes.md: Change verification
- preview-deploy.md: Preview deployment
- kv.md: KV namespace management
Each skill ensures Claude Code performs specific tasks consistently and verifiably.
Hooks
Automatically running hooks:
.claude/hooks/
├── PostToolUse.sh # Formatting after each code change
└── AgentStop.sh # Verification at each agent completion
CLAUDE.md Rules
Project-specific rules are defined in the CLAUDE.md file. This file helps Claude Code understand project rules:
## CRITICAL RULES
### NO HARDCODED VALUES
// WRONG:
const price = '$49.99'
const planName = 'Tier 2 - Professional'
// CORRECT:
Dynamic fetch from API
Real values from database
This approach prevents hardcoded values from leaking into code and ensures a consistent development experience.
Build and Deployment
npm Scripts
# Development
npm run dev # Dev server
# Build
npm run build # Production build
npm run build:kv-data # Search KV data
npm run build:premium-kv # Premium content KV
# Deployment
npm run deploy:pages # Cloudflare Pages
npm run preview # Local preview
Build Process
1. Astro build → dist/
2. Pagefind indexing → dist/pagefind/
3. KV data generation (optional)
4. Cloudflare Pages deployment
Performance Optimizations
Bundle Optimization
// Vite config
- Manual chunk splitting (supabase, paddle separate)
- Asset inline limit: 0 (never inline)
- CSS immutable (1 year cache)
Image Optimization
- Build-time optimization with Sharp
- R2 CDN delivery
- Lazy loading
- WebP/AVIF formats
KV Caching
- Search content: Within PREMIUM_CONTENT
- Premium posts: PREMIUM_CONTENT
- Course content: COURSE_CONTENT
Security
Headers
Content-Security-Policy: [detailed CSP]
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Rate Limiting
// RATE_LIMITER KV namespace
// Per-endpoint throttling
Webhook Signature Verification
// Paddle webhooks are verified with signature
const isValid = verifyPaddleSignature(request, secret);
Metrics
| Metric | Value |
|---|---|
| Astro Version | 5.15.2 |
| Components | 68+ |
| API Endpoints | 15 |
| KV Namespaces | 6 |
| Language Support | 2 (TR, EN) |
| Subscription Tiers | 3 |
| Build Output | ~200MB |
Conclusion
This architecture presents a productizable membership platform template where modern web technologies come together:
- Performance: Astro SSG + Cloudflare edge
- Flexibility: Hybrid rendering, dynamic APIs
- Scalability: KV caching, R2 storage
- Security: CSP, rate limiting, webhook verification
- Developer Experience: Claude Code, slash commands, hooks
- Cost Optimization: Staying within Cloudflare free tier limits
It’s possible to create a cost-efficient solution by staying within Cloudflare’s free tier. However, this requires giving up some advantages of the official adapter. For this reason, when I productize the project, I’ll offer it in two different packages. If you want to stay updated on developments and updates, you can follow me on X.
In summary, What did I build? Not a blog, but a reusable platform template. Who is it for? Developers looking to build a similar membership system. Why is it different? Because performance, security, and cost balance were considered together.