Skip to main content

Quick start: deploy in 15 minutes

This guide walks you through deploying a working Flexweg CMS without compiling anything locally. You'll have a live admin running on your Flexweg site at the end, ready to publish your first post.

The easiest way — Flexweg's one-click auto-installer

Before reading any further, check if your Flexweg account already offers the auto-installer for Flexweg CMS — it deploys the admin into your site in one click, with zero code, no terminal, no dist/ to download:

  1. Open your Flexweg dashboard and select the site you want to install Flexweg CMS on (https://www.flexweg.com/account/sites/<your-site-id>).
  2. If your site is empty, you'll see an Install apps button right on the page — click it.
  3. If the site already has files, click the menu button (next to the site selector at the top right) and pick Install apps in the dropdown.
  4. A modal opens listing all the apps Flexweg offers as one-click installs. Find Flexweg CMS and click Install.
  5. Flexweg drops the admin bundle into your site (under /admin/) and adds a "Flexweg CMS Admin" button to your site's interface — click it any time to jump straight into the admin.

That's it. The first time you open the admin, the in-admin setup form walks you through Firebase + your API key, then you're publishing.

If you've used the auto-installer, you can skip directly to the first-run setup form — the manual steps below are only needed if you want to deploy by hand or you're a developer who wants to build the admin yourself.


Manual deployment (if you can't use the auto-installer)

This guide walks through the same outcome by hand: create a Firebase project, upload dist/admin/ to your Flexweg site, and fill in the setup form.

Prerequisites

Have these ready before you start:

  • A Flexweg account with a site created (your domain — e.g. mysite.flexweg.com)
  • A Google account (for Firebase)
  • A pre-built dist/admin/ folder (downloaded or produced by npm run build)

Allow ~15 minutes total: 5 for Firebase, 5 for Flexweg uploads, 5 for the in-admin setup form.

Step 1 — Create a Firebase project

  1. Open the Firebase Console.
  2. Click Add project → enter a name (e.g. mysite-cms) → accept defaults → wait for creation.
  3. Inside the project, open Build → Authentication → Get started.
  4. On the Sign-in method tab, enable Email/Password.
  5. Switch to the Users tab and click Add user. Use the email + password you want for the admin login. Keep these for step 3.
  6. Open Build → Firestore Database → Create database.
  7. Pick a region close to you (this can't be changed later) → start in production mode (we'll add rules in a moment).
  8. Open Build → Firestore Database → Rules and paste the rules below — replacing [email protected] with the email you set in step 5:
rules_version = '2';

service cloud.firestore {
match /databases/{database}/documents {

// ─── Helpers ──────────────────────────────────────────────────────────
// Bootstrap admin: email pinned here. The single source of truth.
function bootstrapAdminEmail() {
}

function isSignedIn() {
return request.auth != null;
}

// Bootstrap admin: signed in, email matches the pinned address,
// AND email is verified. The email_verified check matters because
// Firebase Email/Password sign-in returns `email_verified: false`
// by default — the SetupForm enforces verification on first run,
// and these rules pin it server-side so an unverified token can
// never sign in as bootstrap admin (defends against future bypass).
function isBootstrapAdmin() {
return isSignedIn()
&& request.auth.token.email != null
&& request.auth.token.email_verified == true
&& request.auth.token.email.lower() == bootstrapAdminEmail();
}

// Reads /users/{uid} for the caller. Returns null if no record yet.
function selfRecord() {
return exists(/databases/$(database)/documents/users/$(request.auth.uid))
? get(/databases/$(database)/documents/users/$(request.auth.uid)).data
: null;
}

function isDisabled() {
let rec = selfRecord();
return !isBootstrapAdmin() && rec != null && rec.disabled == true;
}

function isAdmin() {
let rec = selfRecord();
return isBootstrapAdmin() || (rec != null && rec.role == "admin" && rec.disabled != true);
}

function isEditor() {
let rec = selfRecord();
return isBootstrapAdmin()
|| (rec != null && (rec.role == "admin" || rec.role == "editor") && rec.disabled != true);
}

// ─── users/{uid} ──────────────────────────────────────────────────────
// - Any signed-in editor can read the list (Users page + author lookup).
// - First login: self-create allowed if uid matches and role is "editor".
// - Self update: only `preferences.adminLocale` may change.
// - Role/disabled changes: admin only.
match /users/{uid} {
allow read: if isEditor();

// Self-create on first login. Regular users may only create
// themselves with role "editor". The bootstrap admin (verified
// email + matching pinned address) may self-create with role
// "admin" so the UI shows admin features immediately, no manual
// promotion step needed. Either branch enforces email match and
// disabled=false.
allow create: if isSignedIn()
&& request.auth.uid == uid
&& request.resource.data.email == request.auth.token.email.lower()
&& request.resource.data.disabled == false
&& (
(isBootstrapAdmin() && request.resource.data.role == "admin")
|| (!isBootstrapAdmin() && request.resource.data.role == "editor")
);

allow update: if isAdmin()
|| (
isSignedIn()
&& request.auth.uid == uid
&& !isDisabled()
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(["preferences", "firstName", "lastName", "bio", "avatarMediaId"])
&& (
!("preferences" in request.resource.data.diff(resource.data).affectedKeys())
|| request.resource.data.preferences.adminLocale in ["en", "fr", "de", "es", "nl", "pt", "ko"]
)
);

allow delete: if isAdmin();
}

// ─── posts/{id} (posts + pages) ───────────────────────────────────────
match /posts/{id} {
allow read, write: if isEditor();
}

// ─── terms/{id} (categories + tags) ───────────────────────────────────
match /terms/{id} {
allow read, write: if isEditor();
}

// ─── media/{id} ───────────────────────────────────────────────────────
match /media/{id} {
allow read, write: if isEditor();
}

// ─── settings/site ────────────────────────────────────────────────────
match /settings/{docId} {
allow read: if isEditor();
allow write: if isEditor();
}

// ─── config/admin ─────────────────────────────────────────────────────
// Empty placeholder probed by the admin client to detect bootstrap-
// admin status WITHOUT carrying the email in /admin/config.js. The
// doc itself doesn't need to exist — what matters is the rule: a
// get() succeeds only when isBootstrapAdmin() returns true.
match /config/admin {
allow read: if isBootstrapAdmin();
allow write: if isAdmin();
}

// ─── config/flexweg + other config docs ──────────────────────────────
// API key — read by every editor (publisher needs it), write admin-only.
// The `docId != "admin"` guard ensures editors do not read the probe
// doc above (Firestore rules OR-merge across all matching paths, so
// without this guard editors would silently pass the bootstrap probe).
match /config/{docId} {
allow read: if docId != "admin" && isEditor();
allow write: if docId != "admin" && isAdmin();
}

// ─── Default deny ─────────────────────────────────────────────────────
match /{document=**} {
allow read, write: if false;
}
}
}
  1. Click Publish.
  2. Open Project settings → General → Your apps, click the web app icon (</>), register a web app (any name), and copy the Firebase config object. You'll paste it in step 3.

Step 2 — Upload the admin to Flexweg

  1. Get a copy of dist/admin/:

    • From a release (if available): unzip it.
    • Or from source: git clone <repo> && cd flexweg-cms && npm install --legacy-peer-deps && npm run build produces dist/admin/ and dist/theme-assets/.
  2. Open your Flexweg dashboard → File manager for your site.

  3. Create a folder named admin/ at the root of your site (or any other name — see Renaming the admin folder for obscurity tips).

  4. Upload the entire content of dist/admin/ into that admin/ folder. You should see at the end:

admin/
├── index.html
├── config.js ← null stub, will be filled at step 3
├── external.default.json
├── assets/ ← admin bundle, CSS, fonts
├── runtime/ ← runtime stubs (react.js, cms-runtime.js, …)
├── plugins/ ← per-plugin bundles
├── themes/ ← per-theme bundles
  1. Upload the content of dist/theme-assets/ into the site root (creates /theme-assets/ next to /admin/). These CSS files are referenced by every published page.

  2. Make sure your Flexweg account allows the file extensions used by the admin and themes:

html, js, css, json, xml, txt, md, svg, png, jpg, jpeg, webp, gif, ico, woff, woff2

Some Flexweg accounts have a stricter default — open Settings → Allowed extensions and ensure all of the above are present.

  1. Open Account → API in your Flexweg dashboard and generate a permanent API key. Note your site URL (e.g. https://mysite.flexweg.com). You'll paste both in step 3.

Step 3 — Run the in-admin setup form

  1. Open https://mysite.flexweg.com/admin/ in your browser.
  2. The setup wizard appears (because config.js is still the null stub). Walk through:
    • Welcome step — read the intro, click I have created my Firebase project.
    • Terms step — read and accept the conditions of use.
    • Configuration step — fill the form:
      • Firebase config fields: paste from step 1.10 (apiKey, authDomain, projectId, storageBucket, messagingSenderId, appId)
      • Bootstrap administrator: email + password from step 1.5
      • Flexweg API: API key + site URL + API base URL (defaults to https://www.flexweg.com/api/v1)
  3. Click Save & continue. The form runs through six validation steps (you'll see them in a progress overlay):
    1. Sign in to Firebase with the entered credentials
    2. Verify the signed-in email matches the admin email
    3. Test the Flexweg API key
    4. Write config/flexweg to Firestore
    5. Upload a populated config.js to Flexweg
    6. Sync theme assets (CSS for default + magazine + corporate) to Flexweg
  4. The admin reloads automatically. You're now logged in.

Step 4 — Configure your site

You're in the admin. The first things to set:

  1. Open Settings → General:
    • Site title: shown in headers, RSS feeds, OG tags
    • Description: shown in OG tags and on themes that surface a tagline
    • Site language: BCP-47 (e.g. en, fr, fr-CA). Injected into <html lang> of every published page.
    • Public site URL: e.g. https://mysite.flexweg.com (no trailing slash). Used by sitemaps, RSS and OG tags to build absolute URLs.
  2. Save.

Step 5 — Publish your first post

  1. Open Posts → New post.
  2. Type a title. The slug auto-derives — adjust it if you want.
  3. Use the editor. Type / to open the block inserter (paragraph, heading, image, list, embed, …).
  4. In the right sidebar:
    • Document tab: status, slug, primary category, tags, hero image, author, publication date.
    • Block tab: properties of the block your cursor is in.
  5. Click Publish at the top right.
  6. Watch the publish log — you'll see entries for the post itself plus the cascading regenerations (home page, the category archive, the menu / posts JSON, sitemap, RSS, etc.).
  7. Open https://mysite.flexweg.com/<your-category>/<your-post-slug>.html in another tab. There it is.

Where to next

Local development?

The flow above is the no-build path — the easiest way to deploy. If you want to develop themes/plugins or modify the admin code, start instead with Local development.