Skip to main content
Version: 2.0.0

Manual setup

info

This is the manual way fo setting up FireCMS. You are encourage to simply run yarn create firecms-app instead.

Let's build a very simple CMS that creates a collection of products, with some properties. It includes samples of some advanced features, such as dynamic conditional fields or references (to the same products' collection, for simplicity).

We are defining our Product type for better type checking and code clarity, but it is not compulsory.

Authentication and authorization are also enabled, and make use of the extra field in the authController to check for permissions.

Create a new React app including Typescript:

yarn create vite my-cms --template react-ts

Go into the new directory:

cd my-cms

Install FireCMS and it's peer dependencies:

yarn add firecms@^2.0.0 firebase@^9 @mui/material@^5 @mui/icons-material@^5 @mui/lab@latest @mui/x-date-pickers@^5.0.0-beta.1 @emotion/react @emotion/styled react-router@^6 react-router-dom@^6

You can then replace the content of the file App.tsx with the following sample code.

Remember to replace the Firebase config with the one you get after creating a webapp in the Firebase console.

If you don't know where to find the Firebase app config, check the Firebase setup section

import React, { useCallback } from "react";

import { User as FirebaseUser } from "firebase/auth";
import {
Authenticator,
buildCollection,
buildProperty,
EntityReference,
FirebaseCMSApp
} from "firecms";

import "typeface-rubik";
import "@fontsource/ibm-plex-mono";

// TODO: Replace with your config
const firebaseConfig = {
apiKey: "",
authDomain: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: ""
};

const locales = {
"en-US": "English (United States)",
"es-ES": "Spanish (Spain)",
"de-DE": "German"
};

type Product = {
name: string;
price: number;
status: string;
published: boolean;
related_products: EntityReference[];
main_image: string;
tags: string[];
description: string;
categories: string[];
publisher: {
name: string;
external_id: string;
},
expires_on: Date
}

const localeCollection = buildCollection({
path: "locale",
customId: locales,
name: "Locales",
singularName: "Locales",
properties: {
name: {
name: "Title",
validation: { required: true },
dataType: "string"
},
selectable: {
name: "Selectable",
description: "Is this locale selectable",
dataType: "boolean"
}
}
});

const productsCollection = buildCollection<Product>({
name: "Products",
singularName: "Product",
path: "products",
permissions: ({ authController }) => ({
read: true,
edit: true,
create: true,
delete: true
}),
subcollections: [
localeCollection
],
properties: {
name: {
name: "Name",
validation: { required: true },
dataType: "string"
},
price: {
name: "Price",
validation: {
required: true,
requiredMessage: "You must set a price between 0 and 1000",
min: 0,
max: 1000
},
description: "Price with range validation",
dataType: "number"
},
status: {
name: "Status",
validation: { required: true },
dataType: "string",
description: "Should this product be visible in the website",
longDescription: "Example of a long description hidden under a tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quis bibendum turpis. Sed scelerisque ligula nec nisi pellentesque, eget viverra lorem facilisis. Praesent a lectus ac ipsum tincidunt posuere vitae non risus. In eu feugiat massa. Sed eu est non velit facilisis facilisis vitae eget ante. Nunc ut malesuada erat. Nullam sagittis bibendum porta. Maecenas vitae interdum sapien, ut aliquet risus. Donec aliquet, turpis finibus aliquet bibendum, tellus dui porttitor quam, quis pellentesque tellus libero non urna. Vestibulum maximus pharetra congue. Suspendisse aliquam congue quam, sed bibendum turpis. Aliquam eu enim ligula. Nam vel magna ut urna cursus sagittis. Suspendisse a nisi ac justo ornare tempor vel eu eros.",
enumValues: {
private: "Private",
public: "Public"
}
},
published: ({ values }) => buildProperty({
name: "Published",
dataType: "boolean",
columnWidth: 100,
disabled: (
values.status === "public"
? false
: {
clearOnDisabled: true,
disabledMessage: "Status must be public in order to enable this the published flag"
}
)
}),
related_products: {
dataType: "array",
name: "Related products",
description: "Reference to self",
of: {
dataType: "reference",
path: "products"
}
},
main_image: buildProperty({ // The `buildProperty` method is a utility function used for type checking
name: "Image",
dataType: "string",
storage: {
storagePath: "images",
acceptedFiles: ["image/*"]
}
}),
tags: {
name: "Tags",
description: "Example of generic array",
validation: { required: true },
dataType: "array",
of: {
dataType: "string"
}
},
description: {
name: "Description",
description: "This is the description of the product",
longDescription: "Example of a long description hidden under a tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quis bibendum turpis. Sed scelerisque ligula nec nisi pellentesque, eget viverra lorem facilisis. Praesent a lectus ac ipsum tincidunt posuere vitae non risus. In eu feugiat massa. Sed eu est non velit facilisis facilisis vitae eget ante. Nunc ut malesuada erat. Nullam sagittis bibendum porta. Maecenas vitae interdum sapien, ut aliquet risus. Donec aliquet, turpis finibus aliquet bibendum, tellus dui porttitor quam, quis pellentesque tellus libero non urna. Vestibulum maximus pharetra congue. Suspendisse aliquam congue quam, sed bibendum turpis. Aliquam eu enim ligula. Nam vel magna ut urna cursus sagittis. Suspendisse a nisi ac justo ornare tempor vel eu eros.",
dataType: "string",
columnWidth: 300
},
categories: {
name: "Categories",
validation: { required: true },
dataType: "array",
of: {
dataType: "string",
enumValues: {
electronics: "Electronics",
books: "Books",
furniture: "Furniture",
clothing: "Clothing",
food: "Food"
}
}
},
publisher: {
name: "Publisher",
description: "This is an example of a map property",
dataType: "map",
properties: {
name: {
name: "Name",
dataType: "string"
},
external_id: {
name: "External id",
dataType: "string"
}
}
},
expires_on: {
name: "Expires on",
dataType: "date"
}
}
});

export default function App() {

const myAuthenticator: Authenticator<FirebaseUser> = useCallback(async ({
user,
authController
}) => {

if (user?.email?.includes("flanders")) {
throw Error("Stupid Flanders!");
}

console.log("Allowing access to", user?.email);
// This is an example of retrieving async data related to the user
// and storing it in the controller's extra field.
const sampleUserRoles = await Promise.resolve(["admin"]);
authController.setExtra(sampleUserRoles);

return true;
}, []);

return <FirebaseCMSApp
name={"My Online Shop"}
authentication={myAuthenticator}
collections={[productsCollection]}
firebaseConfig={firebaseConfig}
/>;
}

Run

Then simply run:

yarn dev

You should be able to see your FireCMS instance in your browser, awesome!

danger

Vite uses the default url http://127.0.0.1:5173 for the development server in versions of node < 18.0.0. If you are using a version of node < 18.0.0, you will need to add this url to the authorized domains in the Firebase console. Firebase Auth will require to add this url to the authorized domains in the Firebase console. Alternatively, you can use the url http://localhost:5173.

Enterprise or start-up?

Learn how to get the most out of your project with the help of our consulting team.
Learn more
Sign up to our newsletter to get the latest news and updates. No spam!