Kalqore
All posts

Free stack tutorial

Launch your idea for $0: the free stack, set up step by step

Website, database, API, email and analytics on free plans. How to set up each one, the real limits as of October 2026, and the catches that take a free app offline.

17 min readBy @thebrokenapp (opens in a new tab)

You don’t need a credit card to put an idea in front of real people. Five jobs get an app live: a website, a database, an API, email and analytics. Each has a service with a free plan big enough for your first few thousand users.

Below, each one gets set up in turn, with its exact free limits (checked on 9 October 2026) and the catches the pricing page doesn’t mention.

Free plans end in one of two ways. Some stop your app: Netlify takes the site offline, Supabase turns read-only. Others fail quietly: PostHog drops events, Resend stops at 100 emails a day. You’ll know which is which before it happens.

At a glance

The free stack

Six services for five jobs. Pick one of the two databases, not both.

JobServiceFree planWhen you hit the limit
WebsiteService:NetlifyFree plan:300 credits a month. Each deploy costs 15.When you hit the limit:Every site on your account goes offline until next month
DatabaseService:SupabaseFree plan:500 MB, 50,000 monthly users, 2 projectsWhen you hit the limit:Turns read-only. It also pauses after a week with no activity.
Database, for documentsService:MongoDB AtlasFree plan:512 MB, 100 operations a secondWhen you hit the limit:New writes fail. Reads keep working.
APIService:Cloudflare WorkersFree plan:100,000 requests a dayWhen you hit the limit:Requests fail until midnight UTC
EmailService:ResendFree plan:3,000 a month, at most 100 a dayWhen you hit the limit:Sending is refused until the quota resets
AnalyticsService:PostHogFree plan:1 million events a monthWhen you hit the limit:Extra events are dropped for good

Not sure which database? Start with Supabase, because login and file uploads come with it. Choose MongoDB Atlas if your app is already written around MongoDB.

How the pieces connect

Who talks to whom
Visitor's browser├── Netlify ...... your website├── PostHog ...... analytics├── Supabase ..... login and data└── Worker ....... your API    ├── Resend ... sends email    └── Atlas .... if you use it

One rule keeps this safe. Anything that needs a secret key, like your Resend key or your database password, lives in the Worker. Anything in your website can be read by whoever presses F12. Here’s why that matters.

Your website

Put the website on Netlify

300 free credits a month. On a small site, deploys spend them faster than visitors do.

netlify.com (opens in a new tab)Free · 300 credits a month

Netlify builds your project from GitHub and serves it from servers around the world. Push a change and the live site updates by itself.

What’s free

Accounts created since September 2025 get 300 credits a month, shared by all your projects. Everything your site does costs some:

What you useCredits
Publishing to your live site (a production deploy)Credits:15 each
Traffic (bandwidth)Credits:20 per GB
Requests: every page, image, script and font, from people and botsCredits:2 per 10,000
Server code, including Next.js server renderingCredits:10 per GB-hour of running time
Deploy previews, branch deploys, failed deploys, rollbacks and form submissionsCredits:Free

On its own, 300 credits is 20 deploys, 15 GB of traffic or 1.5 million requests. A real month mixes them: ten deploys (150 credits), 5 GB of traffic (100) and 250,000 requests (50) uses it all. For a small site, that’s a few thousand visits.

Set it up10 min

  1. Push your project to GitHub.
  2. Sign up at netlify.com with your GitHub account.
  3. Click Add new project → Import an existing project, choose GitHub and pick your repository.
  4. Keep the build settings Netlify detects. If your app reads environment variables, like your Supabase URL and publishable key, add them here.
  5. Click Publish. A minute or two later the site is live at your-project.netlify.app.
  6. Make it public. New accounts start every project as private, so everyone else sees a Netlify login page. Go to Project configuration → General → Visitor access → Project visibility and make it public.

No GitHub? Drag your built folder (usually dist or build) onto app.netlify.com/drop. You’ll repeat that for every change.

If refreshing a page gives a 404

Most React and Vite apps handle their routes in the browser, so Netlify doesn’t know /dashboard exists and a refresh there shows “Page not found”. Add a file called _redirects, no extension, to your public folder:

public/_redirects
/*    /index.html    200

Watch out for

  • Deploying on every small change. Every push to your main branch costs 15 credits. An AI agent that commits every few minutes can spend the month in an afternoon. Work on a separate branch, where previews are free, and merge when a batch is ready.
  • One site can take down all of them. When the shared credits run out, every site on your account shows “Site not available” until next month, and the free plan can’t buy more. Netlify emails you at 50, 75, 90 and 100%. Read those emails.
  • Big images and video. Each GB of traffic costs 20 credits. Compress images, and put video on YouTube or Vimeo.
  • Older accounts work differently. Signed up before 4 September 2025? You’re on Netlify’s legacy free plan, with different limits.
Paste into your AI tool
Get this project ready for Netlify. Add a netlify.toml with the right build command and publish folder. If it’s a single-page app, add a _redirects rule that serves index.html for every route. List the environment variables the frontend needs, and flag any secret ones that must move to the backend.
Your database

Store your data in Supabase

Postgres with login and file storage built in. On the free plan, it falls asleep if nobody uses it.

supabase.com (opens in a new tab)Free · 2 projects

Supabase gives you a Postgres database, sign-up and login, file storage, and an API your website can call directly.

What’s free

ResourceFree limit
Database sizeFree limit:500 MB
File storageFree limit:1 GB
Data sent out (egress)Free limit:5 GB a month, plus 5 GB served from cache
Users who log inFree limit:50,000 a month
Edge Function callsFree limit:500,000 a month
ProjectsFree limit:2 active at a time
BackupsFree limit:None

Set it up15 min

  1. Sign up at supabase.com and create a project. Generate a strong database password and save it. Pick the region closest to your users; you can’t change it later.
  2. Create your tables in the Table Editor. Tables made there start with Row Level Security on (more below).
  3. Click Connect at the top of the dashboard and copy the Project URL and the publishable key, which starts with sb_publishable_.
  4. Install the client and connect:
JavaScript · npm install @supabase/supabase-js
// src/supabase.jsimport { createClient } from "@supabase/supabase-js"; export const supabase = createClient(  import.meta.env.VITE_SUPABASE_URL,  import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY); // Anywhere else: the rows this user is allowed to seeconst { data, error } = await supabase.from("notes").select("*");

That’s for Vite. On Next.js, name the variables NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, and read them from process.env.

Watch out for

  • It falls asleep. After 7 days without real activity, a free project is paused and your demo link stops working. Supabase emails a warning first, and Resume project brings it back with your data. An app with daily users stays awake.
  • Past 500 MB, it turns read-only. Users can still see everything, but nothing new saves: no sign-ups, no orders. The usual culprit is files saved into tables. Put uploads in Supabase Storage instead.
  • There are no backups. Delete the wrong table and it’s gone. Keep your own copy. Here’s how.
  • Row Level Security decides who sees what. Your publishable key is public by design. RLS is the only thing stopping anyone who has it from reading every row, so keep it on for every table, including ones made with SQL, which start with it off. Empty results? Usually that’s RLS on with no policy written yet.
  • The secret key never goes in the website. The key starting sb_secret_ (in older projects, service_role) skips every RLS rule. It belongs in your Worker’s secrets only.
  • Login emails won’t reach your users yet. Supabase’s built-in email only reaches your own team, two an hour. Send them through Resend before launch.
Paste into your AI tool
Connect this app to Supabase with @supabase/supabase-js, using the project URL and publishable key from environment variables. Turn on Row Level Security for every table, with policies so each signed-in user can only read and change their own rows. Never use the secret key in frontend code. Show me each policy before applying it.
Or: a document database

Or store documents in MongoDB Atlas

Instead of Supabase, not as well as it. Free forever, as long as something connects at least once a month.

mongodb.com (opens in a new tab)Free · 512 MB

MongoDB stores JSON-like documents instead of rows and columns, and Atlas is its hosted version. Choose it if your code already uses MongoDB or Mongoose.

What’s free

ResourceFree limit
Storage, data and indexes togetherFree limit:512 MB
SpeedFree limit:100 operations a second
ConnectionsFree limit:500 at once
Data transferFree limit:10 GB in and 10 GB out, every 7 days
BackupsFree limit:None

Set it up15 min

  1. Sign up at mongodb.com. On your project’s overview page, click Create and choose the free cluster (it used to be called M0).
  2. Pick a cloud provider and the region closest to your users, then click Create.
  3. In the security quickstart, create a database user with Autogenerate Secure Password, and save the password. Click Add My Current IP Address, then Finish and Close.
  4. Let your live backend in: Database & Network Access → IP Access List → Add IP Address, enter 0.0.0.0/0 and confirm. What that costs you is below.
  5. On the Clusters page, click Connect → Drivers and copy the connection string. Replace <db_password> with your password. That string opens your whole database: store it only as a Worker secret called MONGODB_URI.

From a Cloudflare Worker

Workers can run the official mongodb driver. The one rule: open the connection inside each request and close it after. A Worker can’t reuse a connection from an earlier request, so the usual pattern, one client at the top of the file, answers the first request and then hangs. We tested it.

src/index.js · npm install mongodb
import { MongoClient } from "mongodb"; export default {  async fetch(request, env, ctx) {    // Open a connection for this request only    const client = new MongoClient(env.MONGODB_URI, { maxPoolSize: 1 });    try {      const notes = client.db("myapp").collection("notes");      const latest = await notes        .find()        .sort({ _id: -1 })        .limit(10)        .toArray();      return Response.json(latest);    } finally {      // Close it once the response has gone out      ctx.waitUntil(client.close());    }  },};

New Workers have the Node.js support the driver needs switched on. If yours was created before August 2026, add nodejs_compat to compatibility_flags in wrangler.jsonc.

Watch out for

  • 0.0.0.0/0 means anyone can knock. A Worker has no fixed IP address to add to the list, so allowing every address is the usual fix. That makes your password the only lock: keep it long and random, and out of your website’s code.
  • Old tutorials won’t work. The Atlas Data API was shut down on 30 September 2025. If your AI tool writes code that calls data.mongodb-api.com, it’s out of date; use the driver.
  • Full means no new writes. Past 512 MB, inserts and updates fail with “you are over your space quota”. Reads keep working, so it looks like a bug.
  • It pauses after 30 days without a connection. Resume it from the dashboard. Live apps connect constantly, so this mostly catches demos.
  • There are no backups. Export a copy now and then with mongodump, from MongoDB’s free Database Tools.
Paste into your AI tool
Connect this app’s backend to MongoDB Atlas with the official mongodb driver, reading the connection string from the MONGODB_URI secret. It runs on Cloudflare Workers, so open a new MongoClient inside each request and close it with ctx.waitUntil(client.close()). Don’t use the Atlas Data API; it has been shut down.
Your backend

Run your API on Cloudflare Workers

100,000 free requests a day, and the one place your secret keys are allowed to live.

cloudflare.com/products/workers (opens in a new tab)Free · 100,000 requests a day

A Worker is a small piece of backend code that Cloudflare runs for you, with no server to rent. Your website calls it for anything that needs a secret: sending an email, reaching your database, charging a card.

What’s free

LimitFree plan
RequestsFree plan:100,000 a day, reset at midnight UTC
CPU time per requestFree plan:10 ms. Waiting on a database or another API doesn’t count.
MemoryFree plan:128 MB

Set it up10 min

Terminal
# 1. Create it. Pick: Hello World#    example, Worker only, JavaScript.#    Say no to deploying for now.npm create cloudflare@latest -- my-apicd my-api # 2. Run it at http://localhost:8787npx wrangler dev # 3. Put it live (log in when asked)npx wrangler deploy # 4. Add a secret key. In your code,#    it's env.RESEND_API_KEYnpx wrangler secret put RESEND_API_KEY

Your API is now live at my-api.your-subdomain.workers.dev, and its code is in src/index.js. Never write keys into that file or into wrangler.jsonc, because both end up in git. For wrangler dev, put them in a .dev.vars file and make sure it’s in .gitignore.

Let your website call it

Your site and your API live on different domains, and browsers block calls between domains unless the API allows them. That’s the “CORS error” every first backend runs into. Start src/index.js from this, with your site’s address (and update it if that ever changes):

src/index.js
// The only website allowed to call this API from a browserconst ALLOWED_ORIGIN = "https://your-site.netlify.app"; const cors = {  "Access-Control-Allow-Origin": ALLOWED_ORIGIN,  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",  "Access-Control-Allow-Headers": "Content-Type, Authorization",}; export default {  async fetch(request, env) {    // Before calling another domain, the browser asks permission first    if (request.method === "OPTIONS") {      return new Response(null, { headers: cors });    }     const url = new URL(request.url);     if (url.pathname === "/api/hello") {      return Response.json({ message: "Hello from your API" }, { headers: cors });    }     return new Response("Not found", { status: 404, headers: cors });  },};

Deploy again, and your website can call it:

In your website’s code
const res = await fetch("https://my-api.your-subdomain.workers.dev/api/hello");const { message } = await res.json();

Watch out for

  • The daily limit is a wall. Past 100,000 requests, your Worker returns Cloudflare’s error 1027 until midnight UTC. The $5-a-month plan raises it to 10 million a month.
  • Heavy work hits the 10 ms limit. Resizing images, making PDFs or hashing passwords yourself can cross it, and the request fails with error 1102. Leave passwords to Supabase’s login.
  • It isn’t a normal Node.js server. Most npm packages work, but ones that need native binaries or a real file system don’t. Ask your AI tool for a Workers-compatible alternative.
Paste into your AI tool
Build this app’s backend as a Cloudflare Worker. Read every API key from Worker secrets (env.NAME), never from code or wrangler.jsonc. Allow CORS only from my website’s domain. Keep each request light: no image processing or password hashing. Add a .dev.vars.example that lists every secret it needs.
Your emails

Send email with Resend

3,000 free emails a month. The number that matters is 100 a day.

resend.com (opens in a new tab)Free · 3,000 emails a month

Resend sends the emails your app needs: welcome messages, password resets, receipts and notifications.

What’s free

LimitFree plan
EmailsFree plan:3,000 a month
Emails per dayFree plan:100, reset at midnight UTC
Verified domainsFree plan:3
Newsletter contactsFree plan:1,000
API requestsFree plan:10 a second

Set it up20 min

  1. Sign up at resend.com. To test right away, send from onboarding@resend.dev to the address you signed up with.
  2. Go to Domains → Add Domain and use a subdomain such as send.yourdomain.com, so your app’s sending reputation stays apart from your own inbox.
  3. Copy each record from the Records tab into your domain’s DNS settings, exactly as shown. Most domains verify within 15 minutes; DNS can take up to 72 hours. If your DNS is on Cloudflare, switch the proxy (the orange cloud) off for these records.
  4. Go to API Keys → Create API Key, give it sending access, and save it to your Worker with npx wrangler secret put RESEND_API_KEY.
  5. Send from the Worker:
src/index.js · in your Worker
async function sendEmail(env, to, subject, html) {  const res = await fetch("https://api.resend.com/emails", {    method: "POST",    headers: {      Authorization: `Bearer ${env.RESEND_API_KEY}`,      "Content-Type": "application/json",    },    body: JSON.stringify({      from: "Your App <hello@send.yourdomain.com>",      to,      subject,      html,    }),  });   // 429 means today's 100 (or this month's 3,000) is used up  if (!res.ok) console.error("Email not sent", res.status, await res.text());  return res.ok;} // For example, after someone signs upawait sendEmail(env, user.email, "Welcome to Your App", "<p>Thanks for joining.</p>");

It logs a failure instead of throwing, so a full quota never breaks sign-up itself.

Send Supabase’s login emails through Resend

In Supabase, open Authentication → Email → SMTP Settings, turn on custom SMTP and enter:

FieldValue
HostValue:smtp.resend.com
PortValue:465
UsernameValue:resend
PasswordValue:Your Resend API key
Sender emailValue:An address on your verified domain, like no-reply@send.yourdomain.com

Watch out for

  • 100 a day is the real limit. A launch day with 150 sign-ups means 50 people never get their welcome email. Supabase’s login emails count too. Expecting a spike? The $20-a-month plan has no daily cap.
  • You need your own domain to email anyone else. The resend.dev test address only delivers to you; everyone else gets a 403 error.
  • Never call Resend from the browser. Every visitor could read the API key and send email as you.
  • Add DMARC to stay out of spam. Once the domain verifies, add one more DNS record: TXT, named _dmarc, value v=DMARC1; p=none;. Gmail and Yahoo require it from bulk senders, and it helps everyone else reach the inbox.
Paste into your AI tool
Add email to this app with Resend. Send only from the Cloudflare Worker, using the RESEND_API_KEY secret and a from address on my verified domain. Write a sendEmail helper that logs failures instead of throwing, so a 429 never breaks sign-up, and send a welcome email after sign-up.
Your analytics

See what people do with PostHog

A million events a month, free, with no card on file.

posthog.com (opens in a new tab)Free · 1 million events a month

PostHog shows which pages people visit and where they drop off, with recordings of real sessions, so you can watch someone get stuck on your sign-up form.

What’s free, every month

ProductFree allowance
Product analyticsFree allowance:1,000,000 events
Session replayFree allowance:5,000 recordings
Feature flagsFree allowance:1,000,000 requests
SurveysFree allowance:1,500 responses
Error trackingFree allowance:100,000 errors

You get one project, and data is kept for a year. No card is needed, and without one, PostHog can’t charge you: at the limit, it stops counting instead.

Set it up10 min

  1. Sign up at posthog.com and pick US or EU hosting. Choose EU if most of your users are in Europe; you can’t move a project between them yourself later.
  2. In your project folder, run the setup wizard below. It logs you in, installs PostHog, adds your keys to .env and sets up events for your app’s main actions.
  3. Deploy and click around your live site. Within a minute or so, your visit shows up in PostHog.
Terminal · in your project folder
npx -y @posthog/wizard@latest

The wizard knows Next.js, React, Vue, SvelteKit, Astro and most other frameworks. By hand, it’s npm install posthog-js and this, once, where your app starts. The token is safe in your website: it can send events but not read them.

JavaScript
import posthog from "posthog-js"; posthog.init("<your-project-token>", {  api_host: "https://us.i.posthog.com", // EU: https://eu.i.posthog.com  defaults: "2026-05-30",});

Watch out for

  • Past the limit, data is lost. After 1 million events in a month, the rest are dropped for good, and feature flags stop working until the month resets. PostHog emails you at 80% and 100%.
  • Events add up faster than visitors. Every page view, click and page exit is an event, so one visit can be 10 or 20. A million events is more like 50,000 to 100,000 visits.
  • Ad blockers hide some of your users. Many block PostHog’s default address. Its managed reverse proxy fixes that, free on every plan, with one change to your DNS.
  • Session replays can record personal data. Form fields are masked by default, the rest of the page isn’t. Add the class ph-no-capture to anything showing names, addresses or balances.
Paste into your AI tool
Add PostHog to this app with posthog-js, reading the project token and host from environment variables. Capture pageviews, plus custom events for sign-up, the main action a user takes, and any payment. Add the ph-no-capture class to every element that shows personal data.
Later

When free runs out

Upgrade the one service that ran out, not all six.

Running out is usually good news: people are using the thing. The first paid step, as of October 2026:

ServiceFirst paid planWhat it fixes
NetlifyFirst paid plan:Personal, $9 a monthWhat it fixes:1,000 credits, and you can buy more instead of going offline
SupabaseFirst paid plan:Pro, from $25 a monthWhat it fixes:No pausing, 8 GB of disk, daily backups kept for 7 days
MongoDB AtlasFirst paid plan:Flex, $8 to $30 a monthWhat it fixes:5 GB of storage, and up to 500 operations a second
Cloudflare WorkersFirst paid plan:Paid, $5 a monthWhat it fixes:10 million requests a month, and far more CPU time per request
ResendFirst paid plan:Pro, $20 a monthWhat it fixes:50,000 emails a month, no daily cap, 10 domains
PostHogFirst paid plan:Pay-as-you-go, no base feeWhat it fixes:The same free allowances, then you pay for usage above them. Set a billing limit.

All of it together is about $60 a month, and you’ll rarely need it all at once.

Before you share the link

The free-stack launch checklist

Tick these off before you post the link anywhere.

0 of 17 ticked

01 · Netlify
02 · Supabase
03 · MongoDB Atlas, if you use it
04 · Cloudflare Workers
05 · Resend
06 · PostHog
Everywhere

That’s the whole stack: a website, a database, an API, email and analytics, for the price of a domain name. Each piece stands alone, so when one runs out, you upgrade or swap just that piece.

And when the app has to hold up under real traffic, that’s the kind of system Kalqore builds and runs.

Launching soon?

Want a second pair of eyes on it before it goes live?

Kalqore builds and runs production software. Tell us what you are shipping and we will tell you where it is likely to break.

Let’s talk (opens WhatsApp in a new tab)
All posts