Building a Low-code CRUD on steroids for SaaS apps
Tags: crud, saas, nocode, lowcode
Published: 2022-10-28 09:30:00

---
title: Building a Low-code CRUD on steroids for SaaS apps
published: true
description: What is CRUDAPVLMEIGK and why your SaaS needs it sooner or later?
tags: crud,saas,nocode,lowcode
cover_image: https://yahooder.sirv.com/saasrock/blog/october-2022-update/cover.png
# Use a ratio of 100:42 for best results.
published_at: 2022-10-28 09:30 +0000
---

Watch the preview [here](https://youtu.be/5wKe0T7k4Zk).

What is [*CRUDAPVLMEIGK*](https://www.youtube.com/watch?v=k8xFbWLUDoQ&t=7s) and why your SaaS needs it sooner or later?

Every software revolves around CRUD, supporting at least 1 of these:

- **Create** _(POST)_: Adding a record
- **Read** _(GET)_: Viewing a list of records
- **Update** (PUT): Editing a record
- **Delete** (DELETE): Removing a record

But when building SaaS applications - _especially B2B apps_, you quickly realize that **CRUD** is not enough. There should be something like CRUD**P** if we want to support row-level permissions, CRUDP**V** if we want to add custom views, CRUDPV**L** if we want to have account limits... and so on.

But how can we give steroids to standard CRUD views and forms?

---

## Introducing CRUD 2.0 - aka [CRUDAPVLMEIGK](https://www.youtube.com/watch?v=k8xFbWLUDoQ&t=7s)

- **[C][R][U][D]**: Standard Create, Read, Update, and Delete rows.
- **[P]**ermissions: Should I have permission to [**C**][**R**][**U**] and /or [**D**] this?
- **[A]PI**: REST API to handle CRUD operations
- **Custom [V]iews**: Save filters with a specific view name
- **Subscription [L]imits**: Does my  plan support adding 1 more record?
- **Co[M]ments**: Letting your users interact in a specific record
- **[E]xport**: A standard way to give your users their data _(e.g. from .csv)_
- **[I]mport**: A standard way to quickly upload data _(e.g. to .csv)_
- **Ta[G]s**: Categorize records with custom tags
- **Tas[K]s**: Remind a user to complete a task on a certain record

If your SaaS application is a _CRM_, imagine building the _CRUDAPVLMEIGK_ for every entity/model: **Companies**, **Contacts** and **Opportunities**.

Sooner or later, B2B users will ask for _CRUDAPVLMEIGK_ features for every entity… or churn, as your software is not compliant _(e.g [exporting data](https://gdpr-info.eu/art-20-gdpr/))_.

---

## The solution - SaasRock v0.7 Entity Builder

I'm about this 🤏 close to release SaasRock v0.7.0, which has more than 400 git changes (don't kill me) but also a ton of new features, especially for the Entity Builder.

### Importing/Sharing Entities has never been easier

If you ever need help on debugging your entity properties, workflow, or custom views, just export them and hand the .json file to me:

![/admin/entities](https://cdn-images-1.medium.com/max/1600/1*67t2flfwNaiNz_DyJJ9Iig.png)

### Creating Entities with 1 click (ok 2)

Before SaasRock v0.7.0 I seeded my system's entities _(CRM Contacts & Deals)_, but now, I can start creating Entity Templates that contain entities metadata and how they relate to each other _(e.g. Opportunities belong to Companies)_.

![/admin/entities/templates/manual](https://cdn-images-1.medium.com/max/1600/1*FJ9IZ_Db1pgci-jAeoBHrg.png)

---

## No-code Routes & Blocks

Entities now have **types**:

- **App**: Loads only at _/app/:tenant/:entity_
- **Admin**: Loads only at _/admin/entities/:entity/no-code_
- **All**: Both _app_ and _admin_ _(e.g. CRM for both admins and customers)_

For each Entity that you create, it will generate 9 full-stack routes:

### List Route

- **Features**: Pagination + Filters + Custom views + Export
- **File**: `$entity.__autogenerated/__$entity.tsx`
- **Autogenerated Route**: …/:entity

![List Route](https://cdn-images-1.medium.com/max/1600/1*5MDZjwsosIT5rF3cQ--Rpw.png)

### New Route

- **Features**: Creates a Row + Sets relationships _(parents/children)_
- **File**: `$entity.__autogenerated/__$entity.new.tsx`
- **Autogenerated Route**: …/:entity/new

![New Route](https://cdn-images-1.medium.com/max/1600/1*Fw10c6222nhwS1YkDDWUEw.png)

### Overview Route

- **Features**: Row Details + Workflow + Tags + Tasks + Comments + Share
- **File**: `$entity.__autogenerated/__$entity.$id.tsx`
- **Autogenerated Route**: …/:entity/:id

![Overview Route](https://cdn-images-1.medium.com/max/1600/1*yyBPwcvMyDmtyo9YI75lFQ.png)

### Edit Route

- **Features**: Updates a Row
- **File**: `$entity.__autogenerated/__$entity.$id/edit.tsx`
- **Autogenerated Route**: …/:entity/:id/edit

![Edit Route](https://cdn-images-1.medium.com/max/1600/1*e7Yo4o1rk1x6e2r5T0kErg.png)

### Share Route

- **Features**: Sets permissions _(who can view/update/comment/delete this?)_
- **File**: `$entity.__autogenerated/__$entity.$id/share.tsx`
- **Autogenerated Route**: …/:entity/:id/share

![Share Route](https://cdn-images-1.medium.com/max/1600/1*LuEIh1PVzAniv5S7RrB6qQ.png)

### Public URL Route

- **Features**: Lets unauthenticated users overview the Row
- **File**: `app/routes/public/$entity.$id.tsx`
- **Autogenerated Route**: /public/:entity/:id

![Public URL Route](https://cdn-images-1.medium.com/max/1600/1*glKNHFgK7MnKIyS3iaPX3g.png)

### All-in-one Route

- **Features**: List + Uses [Remix](https://remix.run/) useFetcher to call the **New** and **Edit** route loaders and actions to keep CRUD in the same URL
- **File**: `$entity.__autogenerated/__$entity.all-in-one.tsx`
- **Autogenerated Route**: …/:entity/all-in-one

You can replace the **List** route with this.

![All-in-one Route](https://cdn-images-1.medium.com/max/1600/1*8mJ64nfO6z4bUpHoJHDx_g.png)

### All No-code Routes & Misc Blocks menu

- **Features**: Overview of your entities and their autogenerated routes
- **File**: `app/routes/admin/entities/no-code/index.tsx`
- **Autogenerated Route**: /admin/entities/no-code

![All the No-code Routes & Blocks](https://cdn-images-1.medium.com/max/1600/1*NWoPKpe9XoFdNOq7RMtCtg.png)

### Row Count Stats Block

- **Features**: Show every entity row count + Filters _(Last 30 or 7 days)_
- **File**: `app/routes/admin/entities/no-code/stats/count.tsx`
- **Autogenerated Route**: No autogenerated route, it's up to you to customize and implement it

You could use this at _/app/:tenant/dashboard_ or _/admin/dashboard_ depending on the entity type.

![Row Count Stats Block](https://cdn-images-1.medium.com/max/1600/1*PfShI2VgteRMlNinxkOA4Q.png)

### My Tasks Block

- **Features**: Display my assigned or created tasks across all entity rows
- **File**: `app/routes/admin/entities/no-code/lists/tasks.tsx`
- **Autogenerated Route**: No autogenerated route, you'd need to plug it yourself

![My Tasks Block](https://cdn-images-1.medium.com/max/1600/1*hxeziOI_oFseIKJsalKn9w.png)

### Tenant New Row Route

- **Features**: Same as the previous New route, but on the tenant side it checks if the entity is tracked by your Pricing Plans, and if it does, it checks if the tenant has not an active subscription or it has reached its entity count limit _(monthly or max)_
- **File**: `app/routes/app.$tenant/$entity.__autogenerated/`__$entity.new.tsx
- **Autogenerated Route**: /app/:tenant/:entity/new

![Limits - No subscription](https://cdn-images-1.medium.com/max/1600/1*_dbucJg76XkheP8Hik30xA.png)

---

## Full Customization

You may not need the Entity Builder autogenerated UI, but if you want to keep all its features but customize some (or all) the properties and have them all stored in their corresponding database/prisma model, you can!

I built the routes in a way that has an open API for full customization.

For example, say you want a **Product** model, this is how you can keep your own [prisma](https://www.prisma.io/) model for it while keeping all the Entity Builder features.

**1/9 Create the Entity**

**Name**: _product_, **Slug**: _products_, **Title**: _Product_ and **Plural**: _Products_.

![Creating the Product Entity](https://cdn-images-1.medium.com/max/1600/1*2hpZOuzyzkyge18veaZ5_Q.png)

**2/9 Create the Properties**

In this case I'll just add a **Title** field of type **TEXT** and uncheck the "**Is dynamic field**" value.

![Adding the Title property](https://cdn-images-1.medium.com/max/1600/1*stypUWDLG6BqaBNY18S0Gg.png)

**3/9 Create the Model referencing Row**

Add the properties matching the same Entity Property names that you created _(in this case "**title**")_.

![Product Prisma Model](https://cdn-images-1.medium.com/max/1600/1*-u_l51gYw3-9R0ypTwwJiA.png)

**4/9 Add a reference to Row**

Add a property with the exact Entity name on the Row model _(in this case "**product**")_ as **optional** _(not all rows will be products)_

![Row model Product reference](https://cdn-images-1.medium.com/max/1600/1*lcGdIJRj3b3z_wr717bghw.png)

**5/9 Update your database**

You can either run **`npx prisma db push`** or create a migration for it.

**6/9 Add the product type in RowWithDetails**

Add the "**product**" reference on the "**RowWithDetails**" type. This way you can access to the product values with **`row.product?.title`**.

![RowWithDetails product nullable property](https://cdn-images-1.medium.com/max/1600/1*0-W5ZIJ6cTTQDybDAKovqQ.png)

**7/9 Include the product in the row queries**

This tells [prisma](https://www.prisma.io/) to fetch products on every corresponding Row.

![Prisma Row Include](https://cdn-images-1.medium.com/max/1600/1*DC-iPvcbOeg_kzFWGnCUUQ.png)

**8/9 Go to the autogenerated New route**

It will show an error indicating that we need to customize the UI field _(since is not a dynamic field)_ for full customization.

![product.title needs to be customized](https://cdn-images-1.medium.com/max/1600/1*9m3lg5ihyA5KZb1taYP9xA.png)

**9/9 Add the property UI for it at RowCustomProperties.tsx**

Here we can catch the entity _(**product**)_ property _(**title**)_ and customize the UI as much as we want.

![Catch the UI field](https://cdn-images-1.medium.com/max/1600/1*oEGP1p8e_KvWL0PmiIRr6g.png)

**End result**

![Product row overview](https://cdn-images-1.medium.com/max/1600/1*RtfLiEK1vcf3rFPtklLABg.png)

![SQL](https://cdn-images-1.medium.com/max/1600/1*QQUyMXFj48m36fNGmvJDqg.png)

### Full customization conclusion

9 steps look like a lot of steps to customize product fields. But it's more than that:

- Keeps all the _**CRUDAPVLMEIGK**_ features
- Lets you catch and render _**custom UI**_
- Lets you _**handle data**_ in any way you want

---

## Conclusion

This release should be out in November 1st, join the [discord server](https://discord.com/invite/KMkjU2BFn9) and let me know your thoughts!

Check out [SaasRock](https://saasrock.com/?ref=dev.to&utm_campaing=blog) - The One-Man SaaS Framework, built with Remix, Tailwind CSS, and Prisma.

Or subscribe to [my newsletter](https://saasrock.com/newsletter?ref=dev.to&utm_campaing=blog) for more.