Skip to content

manuelschurr/flutter_bootstrap_multi_user

Repository files navigation

Flutter Multi-User App — Infrastructure Bootstrap Template

Deploy a Flutter web frontend (Cloudflare Pages) + Dart Shelf backend (GCP Cloud Run) + Neon PostgreSQL with automated CI/CD via GitHub Actions, managed by OpenTofu.

Multi-user by default — Google OAuth sign-in, per-user data isolation via PostgreSQL Row-Level Security, and an email allow-list to control who can sign up.

Architecture

 ┌──────────────┐     ┌─────────────────┐     ┌──────────────┐
 │  Cloudflare  │     │  Google Cloud   │     │    Neon      │
 │  Pages       │───▶│  Run            │────▶│  PostgreSQL  │
 │  (Frontend)  │     │  (Backend API)  │     │  (Database)  │
 │  domain.com  │     │  *.run.app      │     │  *.neon.tech │
 └──────────────┘     └─────────────────┘     └──────────────┘
        ▲                      ▲
        │                      │
        └──── GitHub Actions ──┘
              (CI/CD on push to main)

Key features:

  • Google OAuth with server-side redirect flow
  • Email allow-list (allowed_emails table) gates who can register
  • Row-Level Security — every user-scoped table enforces isolation at the database level
  • Two separate Dart projects — Flutter frontend (root) and Dart Shelf backend (server/), no shared code
  • Full IaC — OpenTofu provisions GCP, Cloudflare, and GitHub secrets in one tofu apply

How to use this template

git clone https://github.com/manuelschurr/flutter_bootstrap_multi_user.git . --depth 1
rm -rf .git

After copying, your project should look like:

.github/workflows/                ← the CI/CD workflows
├── deploy-frontend.yml           Flutter web → Cloudflare Pages
└── deploy-backend.yml            Dart server → Cloud Run

lib/                              ← Flutter frontend
├── main.dart                     App entry point + auth token handling
├── screens/
│   ├── auth_page.dart            Sign-in page (Google OAuth button)
│   └── main_page.dart            Items CRUD UI (list, add, edit, delete)
├── providers/
│   └── auth_provider.dart        Auth state (ChangeNotifier via Provider)
└── services/
    ├── auth_service.dart          Auth API client (login, logout, verify)
    └── database_service.dart      Generic REST client (select, insert, update, delete)

server/                           ← Dart backend
├── bin/
│   └── server.dart               Server entry point + middleware pipeline
├── lib/
│   ├── db.dart                   PostgreSQL connection pool + RLS session var
│   ├── middleware/
│   │   └── auth_middleware.dart   Bearer token validation
│   └── routes/
│       ├── auth_routes.dart       Google OAuth + session management
│       └── db_routes.dart         Items CRUD endpoints
├── sql/
│   └── schema.sql                Database schema (users, items, allowed_emails)
├── Dockerfile                    Multi-stage build (dart:stable → scratch)
├── .dockerignore
├── pubspec.yaml
└── pubspec.lock

tofu/                             ← IaC, run `tofu` from here
├── main.tf                       Providers (GCP, Cloudflare, GitHub)
├── variables.tf                  All input variables
├── terraform.tfvars.example      The one file you fill in
├── gcp.tf                        APIs, Artifact Registry, Cloud Run, Secrets
├── iam.tf                        Service account + IAM bindings
├── cloudflare.tf                 Pages project + DNS
├── github.tf                     GitHub Actions secrets (all of them)
├── outputs.tf                    Values needed after apply
└── .gitignore                    Excludes tfvars + state files

web/                              ← Flutter web shell
├── index.html
├── manifest.json
├── favicon.png
└── icons/                        PWA icons

You can then initialize a git repository and start building on top of this template.


Prerequisites (install once)

Tool Install Verify
OpenTofu opentofu.org/docs/intro/install tofu --version
gcloud CLI cloud.google.com/sdk/docs/install gcloud --version
Git git-scm.com git --version

Accounts needed: Google Cloud, Cloudflare (free), GitHub, Neon (free tier).


Step-by-step Setup

1. Manual prep (web consoles)

Do these first — you need the values for terraform.tfvars.

1.1 Cloudflare: domain + API token

  1. Add your domain to Cloudflare (if not already)
    • dash.cloudflare.com → Add a site → enter your domain
    • Update your domain registrar's nameservers to the ones Cloudflare gives you
    • Wait for DNS propagation (~minutes to hours)
  2. Note your Account ID and Zone ID
    • Dashboard → your domain → Overview → right sidebar
  3. Create an API token

1.2 GitHub: Personal Access Token

  1. Go to github.com/settings/tokens
  2. Create a Fine-grained token scoped to your repo
  3. Under Repository permissions, enable: Secrets (read/write), Actions (read/write), Metadata (read)
    • Note: "Secrets" here means Actions secrets — do not confuse with "Codespace secrets" which is a different permission

1.3 Google Cloud: project + OAuth credentials

  1. Create a GCP project at console.cloud.google.com
    • Link a billing account (free tier covers small projects)
    • Note the Project ID (shown on the project dashboard, may have numbers appended)
  2. Configure the OAuth consent screen
    • Go to APIs & Services → OAuth consent screen
    • Set app name, support email, etc.
  3. Create an OAuth 2.0 Client ID (Web application)

1.4 Neon: database

  1. Create a database at neon.tech
  2. Note the connection string (format: postgresql://user:pass@host/db?sslmode=require)
  3. Apply the schema: psql $DATABASE_URL -f server/sql/schema.sql

1.5 GitHub: create your repository

  1. Create a new (private) GitHub repo
  2. Note the owner/repo name (e.g. yourname/myproject)

2. Configure

cd tofu/
cp terraform.tfvars.example terraform.tfvars

Open terraform.tfvars and fill in all values. Everything you need comes from step 1.

Note: project_name must be lowercase with dashes only (Cloudflare Pages requirement). It's used for naming resources across all providers.

⚠️ Never commit terraform.tfvars — it contains secrets. It's already in .gitignore.


3. Authenticate gcloud

gcloud auth application-default login

This gives the terminal and therefore OpenTofu access to create GCP resources.


4. Apply infrastructure

cd tofu/
tofu init
tofu plan     # Review what will be created
tofu apply    # Create everything

This creates (inside your existing GCP project):

  • APIs enabled (Cloud Run, Artifact Registry, Secret Manager)
  • Artifact Registry Docker repository
  • Cloud Run service (skeleton — CI deploys the actual image)
  • Secrets in Secret Manager (no trailing newlines!)
  • GitHub Actions service account + IAM roles
  • Cloudflare Pages project + DNS records
  • All GitHub repository secrets (auto-populated, including CLOUD_RUN_URL)

After apply, run tofu output cloud_run_url to see the backend URL.

Configure Google OAuth redirect URIs — the Cloud Run URL is available immediately (Tofu creates the service with a placeholder image), so OAuth will work on the very first deploy:

  1. Go to Google Cloud Console → Credentials
  2. Edit your OAuth 2.0 Client ID and add:
    • Authorized redirect URIs: <CLOUD_RUN_URL>/api/auth/google/callback
    • Authorized JavaScript origins: https://yourdomain.com

5. Add allowed users

Before anyone can sign in, their email must be in the allowed_emails table:

psql $DATABASE_URL -c "INSERT INTO allowed_emails (email) VALUES ('user@example.com');"

6. Deploy

Push your code to trigger both workflows:

git add .
git commit -m "Initial deploy"
git push origin main

Both workflows read all configuration from GitHub secrets (set by Tofu). No placeholders or manual URL wiring needed.

  • Backend (server/** changes): builds Docker image, pushes to Artifact Registry, deploys to Cloud Run
  • Frontend (lib/**, web/**, pubspec.* changes): builds Flutter web with the Cloud Run API URL baked in, deploys to Cloudflare Pages

Use workflow_dispatch (manual trigger) if path filters don't match on the first push.


7. Verify

  1. <CLOUD_RUN_URL>/health{"status": "healthy"}
  2. https://yourdomain.com → your app loads
  3. Sign in with a Google account that's in the allowed_emails table

Local Development

Backend

cd server
dart pub get

Create a .env file or export the required environment variables:

export ALLOWED_ORIGIN=http://localhost:8080
export FRONTEND_URL=http://localhost:8080
export GOOGLE_CLIENT_ID=<your-google-client-id>
export GOOGLE_CLIENT_SECRET=<your-google-client-secret>
export GOOGLE_AUTH_REDIRECT_URI=http://localhost:8080/api/auth/google/callback
export DATABASE_URL=postgresql://user:pass@host/db?sslmode=require

Then start the server:

dart run bin/server.dart

The backend runs on http://localhost:8080 by default. Override with export PORT=<port>.

Frontend

flutter pub get
flutter run -d chrome --dart-define=API_BASE_URL=http://localhost:8080

API_BASE_URL tells the frontend where the backend is. It defaults to http://localhost:8080 if omitted.

Useful commands

Command Description
flutter analyze Lint the frontend
flutter test Run frontend tests
dart compile exe server/bin/server.dart -o server/bin/server AOT-compile the backend
docker build -t server server/ Build the backend Docker image
psql $DATABASE_URL -f server/sql/schema.sql Apply the database schema

Gotchas (lessons learned)

  1. Secrets: never use echo piped to gcloudecho adds trailing newlines that silently break authentication. OpenTofu's google_secret_manager_secret_version handles this correctly.

  2. No custom domain for the API — Cloud Run domain mapping requires Google to provision a certificate and verify domain ownership. Using the *.run.app URL directly is simpler and works perfectly (users never see the API URL).

  3. Cloudflare Pages project auto-creation — the wrangler pages project create command in the workflow is idempotent. OpenTofu also creates it, so the workflow create is just a safety net.

  4. Cloud Run free tier — 2M requests/month, 360K GB-seconds, 180K vCPU-seconds. More than enough for small projects.

  5. GitHub Actions path filters — workflows only trigger when relevant files change. Use workflow_dispatch (manual trigger) if the first push doesn't trigger both.

  6. GITHUB_TOKEN cannot write repository secrets — the built-in Actions token never has permission to manage secrets, even with elevated permissions. That's why CLOUD_RUN_URL is set by Tofu (which uses your PAT) rather than by the workflow.

  7. Cloud Run service managed as skeleton in Tofu — Tofu creates the service (so the URL is known upfront), but lifecycle { ignore_changes = [template] } lets GitHub Actions manage the actual image, env vars, and scaling config on each deploy.


Extending the Template

This template provides a working multi-user foundation. Common next steps:

  • Replace in-memory sessions with JWTs — the current in-memory token store resets on Cloud Run container restarts. Use signed JWTs (verified by signature, no server state) for persistent sessions.
  • Add roles and authorization — the users table has no role column yet. Add one and extend authMiddleware to check permissions.
  • Add new data tables — follow the items pattern: add a user_id column, enable RLS, create a user_isolation policy, and always pass userId to Db.instance.query().
  • Multi-tenancy — if users need shared workspaces, add organizations + org_memberships tables and shift RLS from user_id to tenant_id.

About

Multi-user app template: Flutter web + Dart Shelf backend + Neon PostgreSQL. Google OAuth, Row-Level Security, email allow-list. Deploys to Cloudflare Pages + Cloud Run via OpenTofu.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors