Logo
CloudWithSingh
Back to all posts
Azure
DevOps

Azure Static Web Apps Is the Most Underrated Service for Developers

Why Azure Static Web Apps should be your default for frontend deployments — free tier, zero-config CI/CD, built-in auth, and API routes included.

Parveen Singh
March 5, 2026
9 min read

Every Azure developer I talk to has the same default: App Service. React app? App Service. Static marketing site? App Service. Documentation portal? App Service.

I was the same way. But honestly, for static frontends and SPAs, App Service is overkill. And you're paying for that overkill every month.

This post walks through Azure Static Web Apps -- what it actually is, how to deploy one from scratch, and why the free tier is genuinely production-ready.

TLDR

Azure Static Web Apps gives you global CDN hosting, auto CI/CD from GitHub, built-in authentication, and serverless API routes -- on a free tier that's actually usable in production. If you're deploying static frontends to App Service, you're overengineering it.

The Problem: App Service for Everything

Here's what deploying a static frontend to App Service looks like:

# Step 1: Create an App Service Plan
az appservice plan create \
  --name my-frontend-plan \
  --resource-group my-rg \
  --sku B1 \
  --is-linux
 
# Step 2: Create the Web App
az webapp create \
  --name my-frontend-app \
  --resource-group my-rg \
  --plan my-frontend-plan \
  --runtime "NODE:20-lts"
 
# Step 3: Configure deployment source
az webapp deployment source config \
  --name my-frontend-app \
  --resource-group my-rg \
  --repo-url https://github.com/you/your-repo \
  --branch main
 
# Step 4: Configure custom domain
az webapp config hostname add \
  --webapp-name my-frontend-app \
  --resource-group my-rg \
  --hostname www.yourdomain.com
 
# Step 5: Add SSL
az webapp config ssl bind \
  --name my-frontend-app \
  --resource-group my-rg \
  --certificate-thumbprint <thumbprint> \
  --ssl-type SNI

Five commands. An App Service Plan you're paying for 24/7. SSL management. No CDN -- that's another service. The B1 plan starts at ~$13/month on Linux. For serving static files.

What Static Web Apps Actually Is

Not just "static file hosting." It's a complete frontend deployment platform:

  • Global CDN -- your content is served from edge nodes worldwide
  • Automatic CI/CD -- push to GitHub, your site deploys
  • Free SSL certificates -- automatic, no configuration
  • Custom domains -- point your DNS and you're done
  • Built-in authentication -- Entra ID, GitHub, Twitter out of the box
  • API routes -- serverless functions alongside your static content
  • Preview environments -- every PR gets its own deployment

The truth is, Microsoft built this service to compete with Vercel and Netlify. And for static/SPA workloads, it does exactly that.

Deploy Your First Static Web App

Let's go from zero to live site. Azure CLI installed, GitHub repo ready.

# Login if you haven't
az login
 
# Create a resource group (or use an existing one)
az group create \
  --name swa-demo-rg \
  --location eastus2
 
# Create the Static Web App
az staticwebapp create \
  --name my-first-swa \
  --resource-group swa-demo-rg \
  --source https://github.com/your-username/your-repo \
  --location eastus2 \
  --branch main \
  --app-location "/" \
  --output-location "build" \
  --login-with-github

One command. Azure connects to your repo, generates a GitHub Actions workflow, triggers the first build, and gives you a live URL at https://<random-name>.azurestaticapps.net. No App Service Plan. No SSL config. No CDN setup.

Local Development with the SWA CLI

# Install the SWA CLI
npm install -g @azure/static-web-apps-cli
 
# Start local dev server with auth + API emulation
swa init
swa start ./build --api-location ./api
 
# Deploy directly from CLI (alternative to GitHub Actions)
swa deploy ./build --deployment-token <your-token> --env production

The swa start command emulates auth and proxies API requests locally. What you see in dev is what you get in production.

The Free Tier Reality

No credit card tricks, no "free for 12 months" caveats:

FeatureFree PlanStandard Plan
Bandwidth100 GB/month100 GB/month (+ $0.20/GB overage)
Storage per environment250 MB500 MB
Total storage500 MB2 GB
Custom domains26
Preview environments310
Apps per subscription10100
SSL certificatesIncludedIncluded
Global CDNIncludedIncluded
CI/CDIncludedIncluded
Built-in auth (pre-configured)IncludedIncluded
Custom auth (OpenID Connect)Not includedIncluded
Private endpointsNot includedIncluded
SLANone99.95%
Price$0/month~$9/month per app

100 GB of bandwidth handles roughly 200,000-500,000 page views per month. For portfolio sites, documentation, and most SPAs, you'll never come close.

SWA Free vs App Service Basic -- The Honest Comparison

CapabilitySWA FreeApp Service B1
Monthly cost$0~$13 (Linux)
Global CDNYesNo (need Azure CDN separately)
Auto SSLYesManual or App Service Managed Certificate
CI/CD setupZero-config (auto-generated)Manual GitHub Actions or Azure DevOps
Custom domains2Custom (depends on plan)
Built-in authEntra ID, GitHub, TwitterNone (code it yourself)
Serverless APIsManaged Functions includedSeparate resource needed
Preview environments3 per appDeployment slots (not on Basic)
Server-side renderingLimitedFull support
WebSocketsNot supportedSupported
Custom runtimeNoYes

Built-in Authentication

This is the feature that honestly sold me. Instead of identity providers, callback handlers, and token management -- you configure auth in a single JSON file:

{
  "auth": {
    "identityProviders": {
      "azureActiveDirectory": {
        "registration": {
          "openIdIssuer": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
          "clientIdSettingName": "AAD_CLIENT_ID",
          "clientSecretSettingName": "AAD_CLIENT_SECRET"
        }
      }
    }
  },
  "routes": [
    {
      "route": "/login",
      "rewrite": "/.auth/login/aad"
    },
    {
      "route": "/dashboard/*",
      "allowedRoles": ["authenticated"]
    },
    {
      "route": "/admin/*",
      "allowedRoles": ["admin"]
    }
  ],
  "responseOverrides": {
    "401": {
      "statusCode": 302,
      "redirect": "/login"
    }
  }
}

Save this as staticwebapp.config.json in the root of your app. That's it. No auth libraries. No token management. No callback URLs.

The pre-configured providers (GitHub, Entra ID, Twitter) work on the Free plan. Route users to /.auth/login/github and SWA handles the entire OAuth flow. Custom OpenID Connect providers (Google, Apple, Auth0) require the Standard plan.

Accessing User Info in Your App

Once a user is authenticated, their info is available at a built-in endpoint:

// In your frontend code
async function getUserInfo() {
  const response = await fetch('/.auth/me');
  const { clientPrincipal } = await response.json();
 
  if (clientPrincipal) {
    console.log(`Logged in as: ${clientPrincipal.userDetails}`);
    console.log(`Provider: ${clientPrincipal.identityProvider}`);
    console.log(`Roles: ${clientPrincipal.userRoles.join(', ')}`);
  }
 
  return clientPrincipal;
}

No JWT decoding. No auth middleware. The platform does it for you.

API Routes with Azure Functions

Create an api folder in your repo and you've got a serverless backend.

my-app/
  src/              # Your frontend code
  api/
    package.json
    host.json
    GetProducts/
      function.json
      index.js
  staticwebapp.config.json

A Simple API Function

// api/GetProducts/index.js
module.exports = async function (context, req) {
  const products = [
    { id: 1, name: "Azure Fundamentals Lab", price: 0, tier: "free" },
    { id: 2, name: "Advanced Networking Lab", price: 29, tier: "premium" },
    { id: 3, name: "Security Operations Lab", price: 29, tier: "premium" }
  ];
 
  // Access authenticated user info (passed automatically by SWA)
  const header = req.headers["x-ms-client-principal"];
  let user = null;
 
  if (header) {
    const encoded = Buffer.from(header, "base64");
    user = JSON.parse(encoded.toString("ascii"));
  }
 
  // Filter based on user role
  const filtered = user?.userRoles?.includes("premium")
    ? products
    : products.filter(p => p.tier === "free");
 
  context.res = {
    status: 200,
    headers: { "Content-Type": "application/json" },
    body: filtered
  };
};
// api/GetProducts/function.json
{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["get"],
      "route": "products"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}

Your frontend calls /api/products and it just works. No CORS. No separate deployment.

Managed Functions limits: HTTP triggers only, 45-second timeout, consumption plan (cold starts), no Durable Functions. The Standard plan lets you "bring your own" Functions app with no restrictions. But managed functions cover 90% of API-behind-frontend use cases.

GitHub Actions Integration

Azure auto-generates this workflow file when you create a Static Web App connected to GitHub:

# .github/workflows/azure-static-web-apps-<random-name>.yml
name: Azure Static Web Apps CI/CD
 
on:
  push:
    branches:
      - main
  pull_request:
    types: [opened, synchronize, reopened, closed]
    branches:
      - main
 
jobs:
  build_and_deploy_job:
    if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
    runs-on: ubuntu-latest
    name: Build and Deploy Job
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: true
          lfs: false
 
      - name: Build And Deploy
        id: builddeploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: "upload"
          # App source code path
          app_location: "/"
          # API source code path - optional
          api_location: "api"
          # Built app content directory
          output_location: "build"
 
  close_pull_request_job:
    if: github.event_name == 'pull_request' && github.event.action == 'closed'
    runs-on: ubuntu-latest
    name: Close Pull Request Job
    steps:
      - name: Close Pull Request
        id: closepullrequest
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          action: "close"

Notice what you didn't have to write: no build config (auto-detected), no deployment credentials (injected automatically), no CDN invalidation, no SSL renewal. Preview environments are created for every PR and cleaned up automatically by the close_pull_request_job.

You can customize the workflow, but the default works out of the box. Zero config really means zero config.

When NOT to Use Static Web Apps

The truth is it's not for everything. Here's when you should reach for something else:

  • SSR-heavy apps -- Next.js/Nuxt.js with full SSR needs a running Node.js server. Use App Service.
  • WebSockets -- real-time chat, live dashboards, collaborative editing. Use App Service.
  • Custom runtimes -- Python, Go, Java backends that aren't Functions-compatible. Use App Service.
  • Microservices / containers -- multiple backend services, KEDA scaling, Docker workflows. Use Container Apps or AKS.
  • Pure static, no APIs -- just HTML behind a CDN with no auth needed. Azure Storage Static Website is even simpler.

The Decision Framework

  1. Does my frontend need a server running? If yes, skip SWA.
  2. Do I need more than HTTP-triggered APIs? If yes, use "bring your own" Functions on Standard, or App Service.
  3. Am I serving more than 100 GB/month? If yes, upgrade to Standard or evaluate your architecture.

All three "no"? Static Web Apps is your answer.

Try It Yourself

Check out the CloudLearn Static Web Apps lab for a hands-on walkthrough with auth and API routes. Or just pick a side project, run az staticwebapp create, and see what happens.

CloudLearn Lab: Deploy Your First Website with Azure CLI
Resource

CloudLearn Lab: Deploy Your First Website with Azure CLI

Build and deploy a real Azure Static Web App step by step using Azure CLI.

cloudlearn.ioLaunch Lab

You'll wonder why you were paying for App Service.

Read Next