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:
- Open your Flexweg dashboard and select the site you want to install Flexweg CMS on (
https://www.flexweg.com/account/sites/<your-site-id>). - If your site is empty, you'll see an Install apps button right on the page — click it.
- 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.
- A modal opens listing all the apps Flexweg offers as one-click installs. Find Flexweg CMS and click Install.
- 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.
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 bynpm 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
- Open the Firebase Console.
- Click Add project → enter a name (e.g.
mysite-cms) → accept defaults → wait for creation. - Inside the project, open Build → Authentication → Get started.
- On the Sign-in method tab, enable Email/Password.
- Switch to the Users tab and click Add user. Use the email + password you want for the admin login. Keep these for step 3.
- Open Build → Firestore Database → Create database.
- Pick a region close to you (this can't be changed later) → start in production mode (we'll add rules in a moment).
- 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() {
return "[email protected]";
}
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;
}
}
}
- Click Publish.
- 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
-
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 buildproducesdist/admin/anddist/theme-assets/.
-
Open your Flexweg dashboard → File manager for your site.
-
Create a folder named
admin/at the root of your site (or any other name — see Renaming the admin folder for obscurity tips). -
Upload the entire content of
dist/admin/into thatadmin/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
-
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. -
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.
- 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
- Open
https://mysite.flexweg.com/admin/in your browser. - The setup wizard appears (because
config.jsis 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)
- Click Save & continue. The form runs through six validation steps (you'll see them in a progress overlay):
- Sign in to Firebase with the entered credentials
- Verify the signed-in email matches the admin email
- Test the Flexweg API key
- Write
config/flexwegto Firestore - Upload a populated
config.jsto Flexweg - Sync theme assets (CSS for default + magazine + corporate) to Flexweg
- The admin reloads automatically. You're now logged in.
Step 4 — Configure your site
You're in the admin. The first things to set:
- 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.
- Save.
Step 5 — Publish your first post
- Open Posts → New post.
- Type a title. The slug auto-derives — adjust it if you want.
- Use the editor. Type / to open the block inserter (paragraph, heading, image, list, embed, …).
- 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.
- Click Publish at the top right.
- 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.).
- Open
https://mysite.flexweg.com/<your-category>/<your-post-slug>.htmlin another tab. There it is.
Where to next
- The admin interface — tour of every section of the admin
- Posts — full post workflow
- Themes overview — pick a theme that fits your site
- Plugins overview — enable / disable / configure plugins
- Operations and updates — keeping the admin up to date
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.