diff --git a/website-astro/src/content/blog/2026-10-29-migrate_from_rowy.md b/website-astro/src/content/blog/2026-10-29-migrate_from_rowy.md new file mode 100644 index 0000000000..9b1bae9bc3 --- /dev/null +++ b/website-astro/src/content/blog/2026-10-29-migrate_from_rowy.md @@ -0,0 +1,650 @@ +--- +slug: migrate_from_rowy +title: "How to Migrate from Rowy to FireCMS: Self-Hosted and Cloud" +seoTitle: "How to Migrate from Rowy to FireCMS" +description: "A step-by-step guide to moving from Rowy, open source or Rowy Cloud, to FireCMS Community, PRO or Cloud. Your data stays in Firestore. A CLI command converts your Rowy tables into FireCMS collections." +pubDate: 2026-10-29 +authors: marian +--- + +Rowy was one of the first tools that made Firestore feel like a spreadsheet. A lot of teams still run their +back office on it. If you are one of them, you have probably noticed that it has stopped moving. + +As of October 2026: + +- The last commit to the Rowy app was in **November 2024**. The last release, v3.0.0, came out in + **October 2023**, under the title "Rowy's biggest release yet + Introducing BuildShip". +- The repository now lives under the BuildShip organization on GitHub (`buildship-ai/rowy`). +- Rowy Run, the backend Rowy deploys into your project, still gets occasional maintenance commits. + The most recent one, in September 2026, pinned dependencies "to prevent breakage". + +Rowy is not shut down. But it is not where its team is putting its effort, and that is a risk for the tool +your operations team uses every day. + +This guide covers every combination: from **self-hosted Rowy** or **Rowy Cloud**, to **FireCMS Community**, +**FireCMS PRO** or **FireCMS Cloud**. We also built a command that does the tedious part for you: + +```bash +npx firecms migrate rowy --project=your-project-id +``` + +It reads your Rowy tables and writes FireCMS collections, plus a report of everything that needs a human. + +## The good news: your data does not move + +Rowy and FireCMS work the same way. Both are an admin UI on top of **your own** Firebase project. Neither keeps +a copy of your data. + +So the documents in Firestore, the files in Cloud Storage, your Firebase Auth users and your security rules +all stay where they are. Nothing gets exported, imported or re-uploaded. + +What you are replacing is everything Rowy added on top: + +| Rowy piece | Where it lives | What replaces it | +|---|---|---| +| Table and column settings | Firestore, under `_rowy_` | FireCMS collections (code or schema editor) | +| The Rowy UI | rowy.app, or your own hosting | FireCMS, self-hosted or Cloud | +| One Cloud Function per table (`R-`) | Your project | Your own Cloud Functions | +| Rowy Run (`rowy-backend`) | Cloud Run, your project | Callable functions, FireCMS entity actions | +| Webhooks (`rowy-hooks`) | Cloud Run, your project | HTTPS Cloud Functions | +| Roles | Firebase Auth custom claims (`roles`) | FireCMS roles, or keep the claims | +| `rowy-service` service account | Your project, key held by Rowy | Nothing. Delete it. (Rowy Cloud only.) | + +There is one detail that makes this migration much safer than it sounds. **The `R-*` functions run on their own.** +They are Firestore triggers deployed in your project. They do not need the Rowy UI. They fire on every write, +including writes made from FireCMS. So you can switch the UI first and port the backend logic afterwards, +one function at a time. + +## Self-hosted Rowy vs Rowy Cloud + +The migration is almost the same for both. The differences are in the cleanup: + +| | Self-hosted Rowy | Rowy Cloud (rowy.app) | +|---|---|---| +| Rowy UI | You host it | Rowy hosts it | +| Rowy Run, `R-*` functions, `rowy-hooks` | In your project | In your project | +| A service account key held by a third party | No | Yes: `rowy-service`, with about 20 admin roles | +| Subscription to cancel | No | Yes, if you are on Pro, Team or Business | + +If you are on Rowy Cloud, read the last section carefully. During setup, Rowy asked you to create a service account +called `rowy-service`. You gave it roles like Project IAM Admin, Service Account Admin, Secret Manager Admin +and Firebase Admin, then uploaded its key to Rowy. That key still works. Deleting it should be part of your plan. + +## Pick your FireCMS flavour + +FireCMS comes in three versions, and they all read the same collection config: + +| | Community | PRO | Cloud | +|---|---|---|---| +| Price | Free, MIT license | €99 / month for the first project, €49 for each extra one. Unlimited users. | €9.99 per user / month, after a 1-month free trial | +| Hosting | You | You | FireCMS | +| Users and roles UI | No: you write an authenticator and permissions in code | Yes | Yes | +| Schema editor UI | No | Yes | Yes | +| CSV and JSON import / export | No | Yes | Yes | +| Change history | No | Yes | Yes | +| Your own React code | Yes | Yes | Yes, uploaded with `npm run deploy` | + +A rough guide: + +- **Coming from self-hosted Rowy**, and you like owning the deployment: FireCMS **Community**, or **PRO** if + non-developers manage users and columns. +- **Coming from Rowy Cloud**, and you want nothing to host: **FireCMS Cloud**. + +The rest of this guide works for all three. Where a step differs, it says so. + +## How Rowy concepts map to FireCMS + +| Rowy | FireCMS | +|---|---| +| Table | Collection | +| Column | Property | +| Section | `group` on the collection | +| Sub-table | Subcollection | +| Collection group table | `collectionGroup: true` | +| Table roles | Role permissions per collection (PRO, Cloud), or a `permissions` builder (Community) | +| Read-only table | `permissions: { read: true, create: false, edit: false, delete: false }` | +| Static default value | `defaultValue` | +| Dynamic default value | `onPreSave` callback, or a Cloud Function | +| Derivative column | A Cloud Function, or `onPreSave` if only FireCMS writes the inputs | +| Formula column | `additionalFields` | +| Action column | `entityActions` that call a callable Cloud Function | +| Extensions | Cloud Functions or Firebase Extensions | +| History snapshot extension | `history: true` (PRO, Cloud) | +| Webhooks | HTTPS Cloud Functions | +| Side drawer | Side panel form | +| Saved filters and sorts | `initialFilter`, `initialSort` | +| Algolia or Meilisearch search | Local text search (Cloud), the [Typesense extension](/blog/full_text_search_firestore_typesense), or a `textSearchController` | + +## Step 1: Back up + +Nothing in this guide deletes data until the last section, but take a backup anyway. A full Firestore export: + +```bash +gcloud firestore export gs://your-backup-bucket/before-rowy-migration --project=your-project-id +``` + +This also captures `_rowy_`, which holds every table setting and every script Rowy ran for you. + +## Step 2: Convert your tables + +Rowy stores its whole configuration in your Firestore: the list of tables in `_rowy_/settings`, and each table's +columns in `_rowy_/settings/schema/{tableId}`. Converting that by hand is slow and full of chances for typos. +So the FireCMS CLI does it: + +```bash +gcloud auth login +npx firecms migrate rowy --project=your-project-id --target=cloud +``` + +Use `--target=self-hosted` (the default) for Community and PRO. The only difference is the import line in the +generated files. + +What the command does and does not do: + +- It **reads** Rowy's configuration: `_rowy_/settings` and the schema documents under it. +- It does **not** read your documents, and it **writes nothing** to your project. +- It signs in with your own `gcloud` account, so you don't need to create a service account key. The account needs + read access to Firestore, for example the Cloud Datastore Viewer role. +- It **never copies webhook verification secrets**. The report just tells you which webhooks had one. + +It writes a `rowy-migration` folder: + +``` +rowy-migration/ +├── MIGRATION_REPORT.md what each column became, and what is left to do +├── collections/ +│ ├── index.ts export const rowyCollections = [...] +│ ├── products.ts +│ └── orders.ts +└── rowy-scripts/ + └── products/ + ├── slug.derivative.ts + ├── publish.action.ts + └── extension-Search_index.ts +``` + +A generated collection looks like the ones you would write by hand: + +```ts +// Generated by `firecms migrate rowy` from the Rowy table "Products" (products). +import { buildCollection } from "@firecms/core"; + +export const productsCollection = buildCollection({ + id: "products", + name: "Products", + path: "products", + group: "Shop", + initialSort: ["title", "desc"], + includeJsonView: true, + properties: { + title: { + dataType: "string", + name: "Title", + validation: { required: true } + }, + status: { + dataType: "string", + name: "Status", + enumValues: [ + { id: "live", label: "live" }, + { id: "draft", label: "draft" } + ] + }, + slug: { dataType: "string", name: "Slug", readOnly: true } + }, + // ... +}); +``` + +A few things it does on purpose: + +- **It never guesses.** When Rowy did not record something, such as which collection a Reference column + points to, the property is left read-only and the report tells you what to set. +- **It keeps Rowy's audit fields alive.** Rowy wrote `_createdBy` and `_updatedBy` on every edit. If any of + your code reads them, it would quietly go stale. The generated `onPreSave` callback keeps writing them. Delete it + if nothing needs them. +- **It turns on the JSON tab** (`includeJsonView`), so editors can see every field of a document, including + the ones without a property. +- **It flags hardcoded credentials.** It copies every derivative, action, extension and webhook script into + `rowy-scripts/` so you can port them. If a line looks like a pasted key or password, the report names the + file and line, without repeating the value. Treat any key it finds as exposed and rotate it. + +No Rowy access, or want to try it first? `--input` reads the same configuration from a JSON file, and +`--tables=products,orders` converts only the tables you name. + +## Step 3a: Self-hosted (Community or PRO) + +Create the app: + +```bash +npx firecms init --community # MIT +# or +npx firecms init --pro # user management, schema editor, import/export, history +``` + +Copy `rowy-migration/collections` into `src/collections/rowy`, and add the collections to the array the template +passes to `useBuildNavigationController`: + +```ts +import { rowyCollections } from "./collections/rowy"; + +const collections = [ + ...rowyCollections + // your other collections +]; +``` + +Run `npm run dev` and sign in with a user from your Firebase project. + +**Who can get in.** With PRO, the user management plugin handles this: recreate your roles and add your users +(see Step 5). With Community, you write an authenticator. Rowy already gave your team a `roles` custom claim, so you +can reuse it: + +```ts +import { Authenticator } from "@firecms/core"; +import { FirebaseUserWrapper } from "@firecms/firebase"; + +export const rowyRolesAuthenticator: Authenticator = async ({ user, authController }) => { + if (!user?.firebaseUser) return false; + const { claims } = await user.firebaseUser.getIdTokenResult(); + const roles = Array.isArray(claims.roles) ? claims.roles as string[] : []; + authController.setExtra({ roles }); + return roles.length > 0; +}; +``` + +Then use `authController.extra.roles` in each collection's `permissions` builder. + +**Deploy** wherever you host static sites. Firebase Hosting is the simplest, see +[Self-Hosted Deployment](/docs/self/deployment). + +## Step 3b: FireCMS Cloud + +1. Go to [app.firecms.co](https://app.firecms.co), create a project and link your **existing** Firebase project. +2. Create the code project. When asked, pick the project you just created: + + ```bash + npx firecms init --cloud + ``` + +3. Copy `rowy-migration/collections` into `src/collections/rowy` and add them in `src/index.tsx`: + + ```ts + import { FireCMSAppConfig } from "@firecms/cloud"; + import { rowyCollections } from "./collections/rowy"; + + const appConfig: FireCMSAppConfig = { + version: "2", + collections: [...rowyCollections] + }; + + export default appConfig; + ``` + +4. Run `npm run dev` to check it locally. Then `npm run deploy` publishes it to your Cloud project. + +**The no-code route.** If you would rather not touch code, skip the CLI. In FireCMS Cloud, create collections +from the schema editor and map them to your existing Firestore collections. "Auto-detect collections" scans +your database and proposes a schema. You lose the things only Rowy's config knew: column order, option lists, +validation, and which columns were derivatives. In exchange you never open a terminal. You can also mix both: +code for your complex tables, the editor for the simple ones. + +## Step 4: Column types, one by one + +Most columns map directly. Here is the full list, and what the CLI does with each. + +| Rowy column | FireCMS property | Notes | +|---|---|---| +| Short text, Phone | `string` | | +| Long text, Code | `string`, `multiline: true` | | +| Rich text | `string`, `multiline: true` | Rowy stored HTML. `markdown: true` would rewrite it as markdown on save, so only use it if your app reads markdown. | +| Markdown | `string`, `markdown: true` | | +| Email / URL | `string`, `email: true` / `url: true` | | +| Single select | `string` with `enumValues` | Rowy's "free text" option allowed values outside the list. FireCMS marks those as invalid. | +| Multi select | `array` of `string` with `enumValues` | | +| Checkbox | `boolean` | | +| Number, Rating, Slider | `number` | Rating and slider ranges become `validation.min` / `max`. | +| Percentage | `number` | Stored as a fraction: 0.25 is 25%. | +| Date / Date & time | `date`, `mode: "date"` / `"date_time"` | | +| Duration | `map` with `start` and `end` dates | | +| GeoPoint | `geopoint` | | +| JSON | `map`, `keyValue: true` | | +| Color | `map` (read-only) | Rowy stored `{ hex, rgb, hsv }`. Editing `hex` alone would leave the others stale. | +| Image / File | read-only list | See "Files and images" below. | +| Reference | `reference` | Set `path` to the target collection to make it editable. | +| Connect table / Connect service | read-only list of `{ docPath, snapshot }` | Convert to `reference` to edit links. See below. | +| Sub-table | subcollection | | +| Array sub-table | `array` of `map` | | +| Derivative | the type it displayed as, read-only | Port its script (Step 5). | +| Formula | not stored | Recreate with `additionalFields`. | +| Action | read-only `map` with the last `status` | Recreate as an entity action. | +| Status | read-only `string` with `enumValues` | When its labels are based on text values. | +| User, Created by, Updated by | read-only `map` | FireCMS can pick users with `userSelect`, which stores only the user ID. | +| Created at, Updated at | read-only `date` inside `_createdBy` / `_updatedBy` | That is where Rowy stored them. | +| ID | not needed | FireCMS always shows the document ID. | + +### Files and images + +Rowy stored each file field as a list of objects: + +```json +[{ "downloadURL": "https://...", "ref": "products/abc/photos/x1-front.jpg", "name": "front.jpg", "type": "image/jpeg", "lastModifiedTS": 1700000000000 }] +``` + +FireCMS stores plain strings: a storage path, or a URL if you set `storeUrl: true`. Until you convert the values, +the generated collection shows these fields read-only, so nothing breaks. + +To make them uploadable, replace each list with its `ref` values. This script does it with the Admin SDK. Without +`--apply` it only counts what would change: + +```ts +// convert-rowy-files.mts +// GOOGLE_CLOUD_PROJECT=your-project-id npx tsx convert-rowy-files.mts products photos [--apply] +import { initializeApp } from "firebase-admin/app"; +import { getFirestore } from "firebase-admin/firestore"; + +const [collectionPath, field, flag] = process.argv.slice(2); +const apply = flag === "--apply"; + +initializeApp(); +const db = getFirestore(); +const snapshot = await db.collection(collectionPath).select(field).get(); +const writer = db.bulkWriter(); + +let changed = 0; +for (const doc of snapshot.docs) { + const value = doc.get(field); + if (!Array.isArray(value) || !value.some(item => item && typeof item === "object")) continue; + const paths = value.map(item => typeof item === "string" ? item : item?.ref).filter(Boolean); + changed++; + if (apply) writer.update(doc.ref, { [field]: paths }); +} +await writer.close(); +console.log(`${changed} documents ${apply ? "updated" : "would change"}.`); +``` + +Then switch the property to the `storage` version the CLI wrote in a comment above it. It keeps Rowy's folder +layout, so new uploads land next to the old ones: + +```ts +photos: { + dataType: "array", + name: "Photos", + of: { + dataType: "string", + storage: { + storagePath: "{path}/{entityId}/{propertyKey}", + fileName: "{rand}-{file}", + acceptedFiles: ["image/*"] + } + } +} +``` + +The files themselves stay where they are. **Check your own apps first.** If they read `photo.downloadURL`, +update them before you run `--apply`. Or map `item.downloadURL` instead of `item.ref` and set `storeUrl: true`, +so the field holds URLs. + +### Connected documents + +Rowy's Connect table column stored a **copy** of each linked document, as `{ docPath, snapshot }`, and the table's +`R-*` function kept those copies in sync. FireCMS links documents with real Firestore references instead. + +If your apps only use `docPath`, convert each value with the same pattern as above, using +`db.doc(item.docPath)`, and use a `reference` property (or an `array` of them). If your apps read fields from +`snapshot`, keep the column read-only, or move the sync into your own Cloud Function before you delete `R-*`. + +## Step 5: Users, roles and security rules + +This is the step people get wrong, so take it slowly. + +**How Rowy did it.** Rowy put a `roles` array in each user's Firebase Auth custom claims, for example +`{ roles: ["ADMIN"] }`. Your Firestore rules check it with a helper Rowy generated: + +``` +function hasAnyRole(roles) { + return request.auth != null && request.auth.token.roles.hasAny(roles); +} +``` + +Table visibility in Rowy was only a UI setting. Your **rules** are what actually protect the data. + +**Keep the claims and the rules for now.** They work no matter which UI sits on top, so your data stays protected +while you switch. With Community and the authenticator from Step 3a, everyone who had a Rowy role can sign in +straight away. With PRO and Cloud, people get in once you add them as users. + +**Add your roles in FireCMS.** The report lists every Rowy role and the tables it could see. In PRO or Cloud, +recreate them in the Roles screen: `ADMIN` and `OWNER` become admins, and the others get permissions on the +collections they could see. In Community, read the claim in your authenticator, as shown in Step 3a. + +**New users need the claim too**, if your rules require it. Rowy Run used to set it. Now it's a few lines with +the Admin SDK: + +```ts +// set-roles.mts +// GOOGLE_CLOUD_PROJECT=your-project-id npx tsx set-roles.mts editor@example.com EDITOR +import { initializeApp } from "firebase-admin/app"; +import { getAuth } from "firebase-admin/auth"; + +initializeApp(); +const [email, ...roles] = process.argv.slice(2); +const user = await getAuth().getUserByEmail(email); +await getAuth().setCustomUserClaims(user.uid, { ...user.customClaims, roles }); +console.log(`${email} now has: ${roles.join(", ")}. They need to sign in again.`); +``` + +To run Admin SDK scripts like these and the file conversion from your machine, sign in once with +`gcloud auth application-default login` and run `gcloud auth application-default set-quota-project your-project-id`. +You won't need a key file. + +If you move to FireCMS PRO, you can later switch your rules to check FireCMS users instead. See +[Firestore rules for PRO](/docs/pro/firestore_rules). FireCMS Cloud keeps its users outside your project, so with +Cloud, keep claims-based rules. + +## Step 6: Port the backend logic + +Everything in this step can wait. The `R-*` functions keep running until you delete them. Port one piece at a +time, deploy it, check it, then turn off the Rowy version. + +### Derivatives + +Rowy ran each derivative inside the table's `R-*` function, on every write. The CLI copied each script to +`rowy-scripts/
/.derivative.ts`. The direct replacement is a Firestore trigger: + +```ts +import { onDocumentWritten } from "firebase-functions/v2/firestore"; + +export const productSlug = onDocumentWritten("products/{id}", async (event) => { + const after = event.data?.after; + if (!after?.exists) return; + + const row = after.data()!; + const slug = String(row.title ?? "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-"); + + // Writing triggers the function again. Stop when there is nothing to change. + if (row.slug === slug) return; + await after.ref.update({ slug }); +}); +``` + +Rowy only re-ran a derivative when one of its listener fields changed. Compare +`event.data.before.data()` with the new data if you need the same behaviour. + +If the inputs are only ever edited in FireCMS, a callback is simpler and needs no deploy: + +```ts +callbacks: { + onPreSave: ({ values }) => ({ + ...values, + slug: String(values.title ?? "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-") + }) +} +``` + +### Actions + +Action columns ran their script on Rowy Run. Move the script into a callable Cloud Function, then add an entity +action that calls it. The action appears in the collection's row menu and in the form: + +```ts +import { getFunctions, httpsCallable } from "firebase/functions"; + +entityActions: [ + { + key: "publish", + name: "Publish", + onClick: async ({ entity, context }) => { + await httpsCallable(getFunctions(), "publishProduct")({ id: entity?.id }); + context.snackbarController.open({ type: "success", message: "Published" }); + } + } +] +``` + +If your action already called one of your own callable functions, the report names it. Only the button needs +recreating. + +### Formulas + +Formulas ran in the browser and were never stored. Use `additionalFields`: + +```ts +additionalFields: [ + { + key: "margin", + name: "Margin", + dependencies: ["price", "cost"], + value: ({ entity }) => Number(entity.values.price ?? 0) - Number(entity.values.cost ?? 0) + } +] +``` + +### Default values + +Static defaults become `defaultValue`, and the CLI does it for you. Dynamic defaults ran in `R-*` when a row was +created. Use `onPreSave` and check `status === "new"`, or a Cloud Function if other apps also create documents. + +### Extensions + +| Rowy extension | Replacement | +|---|---| +| History snapshot | `history: true` on the collection (PRO, Cloud) | +| Algolia index | The official Algolia Firebase Extension | +| BigQuery index | The official "Stream Firestore to BigQuery" extension | +| SendGrid email | The official "Trigger Email from Firestore" extension | +| Meilisearch, Slack, Twilio, push notification, API call, task, doc sync | A Cloud Function with a Firestore trigger | + +Keep secrets in Secret Manager and read them with `defineSecret`. Never put them in the function's code. Rowy stored +its extension secrets in Secret Manager too, so the secrets you need are probably already there. + +### Webhooks + +Rowy's webhooks were served by the `rowy-hooks` Cloud Run service, at URLs like +`https://rowy-hooks-....run.app/wh//`. The report lists each one with its current URL. +For each: + +1. Port the parser from `rowy-scripts/` into an HTTPS function (`onRequest`). +2. Create a new verification secret in Secret Manager. The CLI deliberately did not copy the old one. +3. Change the URL in the sending service (Stripe, Typeform, SendGrid...) and send a test event. + +## Step 7: Run both, then switch + +Because both tools work on the same data, you can run them side by side for as long as you like. FireCMS saves with +a merge, so it never deletes fields it doesn't know about. Columns you have not converted yet are left untouched. + +A sensible order: + +1. Give a few people FireCMS, and keep Rowy for everyone else. +2. Fix what they run into: usually a missing reference `path`, or a select option. +3. Move everyone. Make Rowy read-only, or stop sharing its URL. +4. Port the backend logic (Step 6) at your own pace. + +## Step 8: Decommission Rowy + +Do this last, and in this order. Each step only removes something you have already replaced. + +1. **Delete the `R-*` functions** once you have ported their derivatives, extensions, default values and + Connect table syncing. They are 1st gen functions, in your Rowy Run region: + + ```bash + gcloud functions delete R-products --region=us-central1 --project=your-project-id + ``` + +2. **Delete the webhook service**, once every sender points at your new functions: + + ```bash + gcloud run services delete rowy-hooks --region=us-central1 --project=your-project-id + ``` + +3. **Delete Rowy Run**, once your actions and role management no longer depend on it: + + ```bash + gcloud run services delete rowy-backend --region=us-central1 --project=your-project-id + ``` + + The container images for both services stay in your project's container registry. Delete them too if you want + to reclaim the space. + +4. **Rowy Cloud only: revoke Rowy's access.** Delete the project in your rowy.app workspace settings and cancel the + subscription. Then delete the `rowy-service` service account. This revokes the key you uploaded to Rowy, along + with all its admin roles: + + ```bash + gcloud iam service-accounts delete rowy-service@your-project-id.iam.gserviceaccount.com --project=your-project-id + ``` + + **Self-hosted Rowy:** take down wherever you hosted the Rowy app. + +5. **Review Secret Manager.** Delete the secrets that only Rowy used. + +6. **Clean up your security rules.** Remove the `_rowy_` rules. Keep `hasAnyRole` and the `roles` claims for as long + as your rules use them. + +7. **Delete `_rowy_`.** Keep your backup from Step 1. This collection holds your old scripts, and + `_rowy_/userManagement/users` holds a copy of each Rowy user's name and email. Don't keep personal data you no longer + need: + + ```bash + firebase firestore:delete _rowy_ --recursive --project=your-project-id + ``` + +## What you gain, and what you'll miss + +What you'll miss is mostly the spreadsheet feel. Formulas live in code now, and so do derivatives. You give up the +"write a function in the browser and click deploy" workflow, in exchange for Cloud Functions you deploy, review and +version yourself. + +What you gain: + +- **Code you own.** Collections are TypeScript in your repository, reviewed in pull requests, the same in every + environment. The schema editor is there for the people who prefer it. +- **A real form for every document,** with validation, conditional fields, custom React fields, and card and kanban + views. +- **Permissions per role and per collection,** with separate read, create, edit and delete. +- **Change history** for each document, with who changed what and a way to revert (PRO, Cloud). +- **A tool under active development,** with its code on GitHub. + +## FAQ + +**Do I have to move my data?** +No. FireCMS reads and writes the same Firestore documents Rowy did. + +**Can I keep using Rowy while I migrate?** +Yes. Both can run at the same time on the same data, and the `R-*` functions keep firing on writes from FireCMS. + +**Will FireCMS delete fields it doesn't have a property for?** +No. Saves are merged into the document, so other fields are left alone. You can see them in the JSON tab. + +**Does `firecms migrate rowy` change anything in my project?** +No. It only reads `_rowy_`. Everything it writes goes into a local folder. + +**I use BuildShip with Rowy's trigger extension. What now?** +Keep your BuildShip workflows. Call them from a Cloud Function with a Firestore trigger, or from a FireCMS +callback or entity action. + +**What if my table settings use a column type the CLI doesn't know?** +The column is left out and listed in the report. Its data stays in the document, and the JSON tab shows it. + +If you get stuck, [tell us on Discord](https://discord.gg/fxy7xsQm3m) or open an issue on +[GitHub](https://github.com/firecmsco/firecms). We'd like to hear which parts of your Rowy setup were hardest +to move.