CI/CD for MVPs: GitHub Actions With Nuxt and Firebase
Most MVP teams deploy manually. Someone runs npm run build, uploads files, or pushes to a branch that triggers a Vercel deploy. It works — until it doesn't. A build with a broken import. A deploy that overwrites a working version. A regression nobody caught because there was no test run.
A basic CI/CD pipeline prevents these problems. With GitHub Actions, setting one up for a Nuxt + Firebase project takes under two hours and saves far more than that in the first few weeks.
Why CI/CD Is Worth Setting Up Even for a Two-Person Team
The argument against CI/CD for MVPs usually goes: "We're too small and too fast-moving for that overhead." But the overhead of CI/CD is a one-time cost. The overhead of shipping broken code is recurring.
What a simple pipeline gives you:
- Confidence — every push is checked before it reaches production
- Speed — no manual deploy steps; merging to main triggers deployment automatically
- History — a log of every deploy, what changed, and whether tests passed
- Preview environments — feature branches can get their own deployed URL for review
For a Nuxt + Firebase project, the pipeline you need is straightforward: run linting and tests on every pull request, deploy to Firebase Hosting on every merge to main.
The Pipeline You Need: Lint, Test, Build, Deploy
A minimal, useful pipeline for an MVP:
On pull request:
1. Install dependencies
2. Run linting (ESLint)
3. Run type checking (tsc)
4. Run unit tests (Vitest)
5. Build the project (verify it compiles)
On merge to main:
1. Build the project
2. Deploy to Firebase Hosting
That's it. No staging environments, no multi-region deploys, no approval gates — those come later. This pipeline prevents the most common problems without adding ceremony.
GitHub Actions Basics: Triggers, Jobs, and Steps
GitHub Actions workflows are YAML files in .github/workflows/. They trigger on GitHub events (push, pull_request, workflow_dispatch), define jobs that run on virtual machines, and steps within each job that run shell commands or pre-built actions.
The structure:
name: Workflow Name
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
job-name:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Step name
run: some-command
Jobs run in parallel by default. If one job depends on another completing successfully, add needs: [job-name].
Writing the Workflow: Nuxt Build and Firebase Hosting Deploy
Create .github/workflows/deploy.yml:
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Type check
run: npm run typecheck
- name: Test
run: npm run test
deploy:
runs-on: ubuntu-latest
needs: [test]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
NUXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }}
- name: Deploy to Firebase
uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
channelId: live
projectId: your-firebase-project-id
The needs: [test] ensures deployment only happens after the test job succeeds. The if: condition restricts deployment to pushes to main, not pull requests.
Securing Your Pipeline: GitHub Secrets and Firebase Service Accounts
The workflow above uses two secrets: FIREBASE_API_KEY and FIREBASE_SERVICE_ACCOUNT. These are stored in your GitHub repository's Settings → Secrets and variables → Actions.
FIREBASE_SERVICE_ACCOUNT is the one that needs setup:
- Go to Firebase Console → Project Settings → Service accounts
- Click "Generate new private key" — download the JSON file
- Copy the entire JSON content
- Add it as a GitHub secret named
FIREBASE_SERVICE_ACCOUNT
The GITHUB_TOKEN is automatically provided by GitHub Actions — you don't need to create it.
For other environment variables your Nuxt app needs at build time, add them as GitHub secrets and reference them in the workflow's env: block, as shown with NUXT_PUBLIC_FIREBASE_API_KEY above.
What to Add Next: Preview Channels, Slack Notifications, Coverage
Once the basic pipeline is running, the most useful additions:
Firebase preview channels — deploy pull requests to temporary URLs for review before merging. The FirebaseExtended/action-hosting-deploy action supports this with channelId: ${{ github.head_ref }} instead of live.
Slack or email notifications on failure — add a step at the end of the deploy job that runs if: failure() and sends a notification. The slackapi/slack-github-action handles this in a few lines.
Test coverage reporting — Vitest can output a coverage report; upload it as a GitHub Actions artifact or use a coverage service like Codecov.
None of these are urgent. Get the basic pipeline running first, validate that it's catching real issues, then add features based on what your team actually needs.
A working CI/CD pipeline is one of those things that feels like overhead until the first time it prevents you from shipping a broken deploy. Let's talk if you want it set up as part of your MVP.