I’ve been building goat.today with a stack that costs $0/month and outperforms React in every benchmark. Here’s why I chose Astro + Solid.js + Supabase + Cloudflare Pages and the real patterns that emerged.

TL;DR: Why This Stack?

ConcernThis StackReact + Vercel + Firebase
Cost$0/month$0-25+/month
JS Bundle~7KB (Solid)~40KB+ (React)
Runtime Performance50-70% fasterBaseline
Cold StartsNone (edge)Yes (serverless)
Free Tier LimitsExtremely generousRestrictive

The $0 Stack Breakdown

Cloudflare Pages: Unlimited Static, 100K Functions/Day

Cloudflare’s free tier is absurdly generous:

ResourceFree Limit
Static requestsUnlimited
BandwidthUnlimited
Function requests100,000/day
Builds500/month
Custom domains100

Compare this to Vercel’s free tier (100GB bandwidth, 100K function invocations/month) or Netlify (100GB bandwidth, 125K function invocations/month). Cloudflare gives you 100K function requests per day, not per month.

Zero cold starts. Functions run on Cloudflare’s edge network, not in a container that needs to spin up.

Supabase: 500MB Database + Auth + Storage

Supabase’s free tier includes everything you need:

ResourceFree Limit
Database500 MB
Storage1 GB
Auth users50,000 MAUs
Edge Functions500K invocations/month
Realtime connectionsIncluded

That’s a full Postgres database with Row Level Security, authentication with OAuth providers, file storage, and realtime subscriptions—for free.

Firebase’s free tier? 1GB storage, 50K reads/day, 20K writes/day. You’ll hit those limits fast.

Total Monthly Cost: $0

Until you have serious traffic (100K+ daily users), you won’t pay a dime.


Why Solid.js Over React?

50-70% Faster Runtime Performance

According to the JS Framework Benchmark, Solid.js consistently outperforms React by 50-70% in real-world scenarios:

  • DOM updates: Solid updates only what changed. React re-renders entire component trees.
  • No Virtual DOM: Solid compiles to direct DOM operations. No diffing overhead.
  • Fine-grained reactivity: Signals update at the value level, not the component level.

6x Smaller Bundle Size

FrameworkRuntime Size
React + ReactDOM~40KB gzipped
Solid.js~7KB gzipped

That’s 33KB less JavaScript your users need to download, parse, and execute.

React-like DX

If you know React, you know Solid:

1
2
3
4
5
6
7
// React
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;

// Solid - almost identical
const [count, setCount] = createSignal(0);
return <button onClick={() => setCount(count() + 1)}>{count()}</button>;

The only difference: signals are functions (count() instead of count). That’s it.


Why Astro Over Next.js?

Zero JavaScript by Default

Astro ships 0KB of JavaScript for static content. Next.js ships the React runtime even for static pages.

Real-world comparison:

  • Astro: ~40% faster initial load, ~90% less JS than comparable Next.js
  • Next.js 15: Better with React Server Components, but still ships more JS

Island Architecture

Instead of hydrating the entire page, Astro only hydrates interactive components:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
---
// This is server-only. No JS shipped.
const posts = await fetchPosts();
---

<h1>Latest Posts</h1>
{posts.map(post => <PostCard post={post} />)}

<!-- Only this component gets JavaScript -->
<LikeButton client:visible postId={post.id} />

The PostCard components are pure HTML. Only LikeButton gets hydrated, and only when it scrolls into view.

Use Any Framework (or None)

Astro doesn’t lock you into React. I use Solid.js for interactive islands, but you can mix React, Vue, Svelte, or vanilla JS in the same project.


The Architecture

Project Structure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
src/
├── components/
│   ├── astro/          # Static components (0 JS)
│   └── solid/          # Interactive islands
├── layouts/
│   └── BaseLayout.astro
├── pages/
│   ├── [locale]/       # i18n routes
│   └── api/            # API endpoints
├── lib/
│   └── supabase/       # DB client
└── middleware.ts       # Auth + i18n

Astro Config

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// astro.config.mjs
import { defineConfig } from 'astro/config';
import solidJs from '@astrojs/solid-js';
import cloudflare from '@astrojs/cloudflare';

export default defineConfig({
  integrations: [solidJs()],
  output: 'server',
  adapter: cloudflare({
    imageService: 'compile',
    platformProxy: {
      enabled: process.env.ENABLE_CF_PROXY === '1',
    },
  }),
  i18n: {
    defaultLocale: 'ko',
    locales: ['en', 'ko', 'es'],
    routing: { prefixDefaultLocale: true },
  },
  prefetch: {
    defaultStrategy: 'tap', // Prevents 503 errors from aggressive prefetching
    prefetchAll: false,
  },
});

Pro tip: Keep platformProxy.enabled: false during development. The dev server starts 10x faster without Cloudflare emulation.


SSR Authentication with Supabase

The trickiest part of this stack is getting auth right across server and client.

Middleware: Single Source of Truth

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr';

const supabaseMiddleware = defineMiddleware(async (context, next) => {
  const supabase = createServerClient(
    import.meta.env.PUBLIC_SUPABASE_URL,
    import.meta.env.PUBLIC_SUPABASE_ANON_KEY,
    {
      cookies: {
        getAll() {
          return parseCookieHeader(context.request.headers.get('Cookie') ?? '');
        },
        setAll(cookiesToSet) {
          context.locals.supabaseCookies = cookiesToSet;
        },
      },
    }
  );

  context.locals.supabase = supabase;
  
  const { data: { user } } = await supabase.auth.getUser();
  context.locals.user = user;

  const response = await next();

  // Apply refreshed cookies to response
  for (const { name, value, options } of context.locals.supabaseCookies ?? []) {
    response.headers.append('Set-Cookie', serializeCookieHeader(name, value, options));
  }

  return response;
});

Passing Auth to Client Components

1
2
3
4
5
6
---
const { user } = Astro.locals;
---

<!-- Pass initial user to prevent flash of unauthenticated content -->
<AuthButton client:load initialUser={user} locale="en" />

Type-Safe Everything

Generate TypeScript types from your Supabase schema:

1
supabase gen types typescript --project-id your-project > src/types/database.ts

Now you get full autocomplete:

1
2
3
4
const { data } = await supabase
  .from('posts')  // ← autocomplete
  .select('id, title, author:profiles(username)')  // ← type-safe
  .eq('is_deleted', false);

Performance Optimizations

1. Early Hints

Preconnect to external resources:

1
2
3
4
5
const earlyHintsMiddleware = defineMiddleware(async (context, next) => {
  const response = await next();
  response.headers.append('Link', '<https://your-project.supabase.co>; rel=preconnect');
  return response;
});

2. Vite Prebundling

Speed up dev server by prebundling heavy dependencies:

1
2
3
4
5
vite: {
  optimizeDeps: {
    include: ['solid-js', 'solid-js/web', '@supabase/supabase-js'],
  },
},

3. Selective Hydration

Use Astro’s client directives wisely:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!-- Load immediately (above the fold) -->
<Navbar client:load />

<!-- Load when visible (below the fold) -->
<Comments client:visible />

<!-- Load when browser is idle -->
<Analytics client:idle />

<!-- Never hydrate (static) -->
<Footer />

Deployment

wrangler.toml

1
2
3
4
name = "your-app"
pages_build_output_dir = "./dist"
compatibility_date = "2025-01-17"
compatibility_flags = ["nodejs_compat"]

GitHub Actions (Optional)

Connect your repo to Cloudflare Pages dashboard, or use GitHub Actions:

1
2
3
4
5
6
- uses: cloudflare/pages-action@v1
  with:
    apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
    projectName: your-app
    directory: dist

When NOT to Use This Stack

  • Heavy client-side interactivity (dashboards, Figma-like apps): Consider pure Solid.js SPA
  • React ecosystem lock-in: If you need specific React libraries, stick with Next.js
  • Team familiarity: If your team knows React and has no time to learn, use what you know

Conclusion

This stack gives you:

FeatureBenefit
$0/monthCloudflare + Supabase free tiers
50-70% fasterSolid.js over React
90% less JSAstro’s island architecture
Zero cold startsCloudflare edge functions
Full-stackAuth, DB, storage, realtime
Type-safeTypeScript everywhere

It’s not the simplest stack. But for a production app with auth, i18n, database, and global edge deployment—all for free—it’s hard to beat.


Check out goat.today to see it in action.