Thuta Learning
ProjectsDevOps & Toolsbeginner

Project: Deploy a Full-Stack App with a Database

What you'll walk away with

  • Explain the core ideas behind Project: Deploy a Full-Stack App with a Database
  • Read the diagram and trace how a request or data flows through the architecture
  • Explain what this means for your own project's decisions

Build the mental model

A full-stack app splits into at least three pieces: a frontend, an API/backend, and a database — and in production, each piece can live on a different platform, deployed independently.

  • Static-vs-dynamic hosting split — which piece is a build output, which is a running process
  • Environment variables and secrets — how the pieces find and authenticate to each other
  • Production databases — managed, backed up, reached only through a connection string
  • CI/CD basics — deploying automatically whenever the relevant code changes

The frontend, if it's a typical React/Vue/Next.js app, is often static or near-static output, deploying the same way as the static site project. The backend is different: a long-running or serverless process that talks to a database and holds secrets, so it deploys to a different kind of platform.

WhereWhat lives there
Frontend (build time)A public URL variable like NEXT_PUBLIC_API_URL, baked into the client bundle so the browser knows where the API is.
Backend (runtime)Private variables: DATABASE_URL, JWT_SECRET, third-party API keys — never exposed to the browser.

Production databases are a managed service

Unlike a local SQLite file or a database in a laptop's Docker container, a production database typically handles backups, connection pooling, and failover for you — the backend just authenticates to it with a connection string.

This is where CI/CD matters again: a working pipeline deploys the frontend and backend automatically whenever their code changes, so "deploy" becomes something that happens the same way on every merge to main.

text
FULL-STACK APP: FRONTEND + API + DATABASE
-----------------------------------------
[BROWSER]
    |
    | HTTPS request
    v
[FRONTEND]  (static/edge host, e.g. a React or Next.js build)
    |  env: NEXT_PUBLIC_API_URL=https://api.example.com
    |
    | fetch() to API_URL
    v
[API / BACKEND]  (separate deploy: container host or PaaS)
    |  env: DATABASE_URL=postgres://...
    |  env: JWT_SECRET=...
    |
    | SQL query over a TLS connection
    v
[DATABASE]  (managed production database)

Connect it to a real scenario

Separate the codebase

A frontend directory and a backend/API directory (or two repos), each independently deployable.

Deploy the backend and set its private env vars

Provision a production database, then set DATABASE_URL, API keys, and signing secrets directly in the platform's dashboard — never in git.

Deploy the frontend and set its public env var

Set the API's public URL using the framework's required public prefix so it bakes into the build.

Wire up CI/CD for both

Connect both deploys to the git repository so a push to main redeploys automatically.

Test the full path in production

Open the live frontend, trigger a request that hits the API and reaches the database — not just a local test.

The #1 full-stack deployment failure

A .env file with all the right values sitting locally, never configured on the actual hosting platform. The app works perfectly on your machine and throws confusing errors — or silently misbehaves — the moment it's live.

Try the working example

javascript
function startApp(env, required) {
  const missing = required.filter((key) => !env[key] || env[key].trim() === "");

  if (missing.length > 0) {
    throw new Error(
      `Cannot start: missing required environment variable(s): ${missing.join(", ")}`
    );
  }

  console.log("Environment OK. Starting server...");
  console.log(`  API_URL      = ${env.API_URL}`);
  console.log(`  DATABASE_URL = ${env.DATABASE_URL.replace(/:[^:@]+@/, ":****@")}`);
  return true;
}

const required = ["API_URL", "DATABASE_URL", "JWT_SECRET"];

const localEnv = {
  API_URL: "http://localhost:4000",
  DATABASE_URL: "postgres://user:pass@localhost:5432/app",
  JWT_SECRET: "dev-secret",
};

const prodEnvMissingSecret = {
  API_URL: "https://api.example.com",
  DATABASE_URL: "postgres://user:pass@prod-db.example.com:5432/app",
};

console.log("--- Attempting local config ---");
startApp(localEnv, required);

console.log("\n--- Attempting production config ---");
try {
  startApp(prodEnvMissingSecret, required);
} catch (err) {
  console.error(err.message);
}
You should see
--- Attempting local config ---
Environment OK. Starting server...
  API_URL      = http://localhost:4000
  DATABASE_URL = postgres://user:****@localhost:5432/app

--- Attempting production config ---
Cannot start: missing required environment variable(s): JWT_SECRET

5-minute try-it

List the environment variables a simple full-stack app (frontend + Express-style API + Postgres database) would need, split into two columns — "frontend, public" and "backend, private" — and for each one, name which platform's dashboard it would actually be set in.

One important caution

Setting an environment variable in a local .env file and forgetting it also needs to be configured, by hand, inside each deployment platform's dashboard.

Using a private secret (like a database password) as a public/client-side environment variable by mistake, exposing it in the frontend's built JavaScript bundle.

The Twelve-Factor App — ConfigCloud & Deployment

Easy traps

  • Setting an environment variable in a local .env file and forgetting it also needs to be configured, by hand, inside each deployment platform's dashboard.
  • Using a private secret (like a database password) as a public/client-side environment variable by mistake, exposing it in the frontend's built JavaScript bundle.
  • Never assume that working on localhost means it will work in production -- environment, network, database, and security differences can all bite.

Exercise

List the environment variables a simple full-stack app (frontend + Express-style API + Postgres database) would need, split into two columns — "frontend, public" and "backend, private" — and for each one, name which platform's dashboard it would actually be set in.

You'll know it worked when: --- Attempting local config --- Environment OK. Starting server... API_URL = http://localhost:4000 DATABASE_URL = postgres://user:****@localhost:5432/app --- Attempting production config --- Cannot start: missing required environment variable(s): JWT_SECRET

Project: Deploy a Full-Stack App with a Database | Thuta Learning