# Adding & Managing Elements Source: https://docs.saasio.io/editor-essentials/adding-elements Learn how to build your UI by adding elements from the Elements palette, using UI Libraries, and organizing them with the Layers panel. *** Elements are the visual building blocks of your application. Every piece of text, every button, and every pre-built component is an element. Learning how to add, organize, and structure them is the key to creating a beautiful and functional UI. *** ## Where to Find Elements In Saasio, you can add elements to your page from two primary sources in the top navigation bar: This palette contains the fundamental building blocks for any UI: **Text**, **Containers**, **Images**, **Buttons**, **Forms**, and more. You'll use these for creating custom layouts and basic content. This tab gives you access to a rich collection of pre-built, professionally designed components from popular libraries like [Shadcn](https://ui.shadcn.com/), [Magic UI](https://magicui.design/), and [Aceternity UI](https://ui.aceternity.com/), etc. Use these to build modern interfaces quickly. *** ## Adding Elements to the Canvas The primary way to build your UI is by dragging and dropping elements onto the central canvas. Open either the **Elements** tab or the **UI Libraries** tab from the top navigation bar. Find the component you wish to add. Click and hold the element, drag it over the central canvas to your desired position, and release it. The new element will appear on your page. After adding an element, look at the **Layers** panel on the left. You will see your new component listed in the page's element hierarchy. The Layers panel is essential for understanding the structure of your page. For faster building, you can use keyboard shortcuts to add common elements directly to your canvas. Press the key combination, and the element will be added instantly. * `Alt + C` : Container * `Alt + T` : Text * `Alt + I` : Image * `Alt + V` : Video * `Alt + 1` : Heading 1 (H1) * `Alt + 2` : Heading 2 (H2) * `Alt + 3` : Heading 3 (H3) * `Alt + L` : Link * `Alt + F` : Form * `Alt + B` : Button * `Alt + Shift + I` : Input * `Alt + A` : Audio *** ## The Power of Containers: Grouping & Nesting The most important element for creating clean, organized layouts is the **Container**. A container is a special element that acts as a box to hold and group other elements. **Key Concept:** To create professional layouts, you should always place related elements inside a **Container**. For example, you would place a title, a paragraph, and a button together inside one container to create a "hero section". To nest an element, simply drag it from the Elements tab and drop it directly **on top of a container** in the canvas. You can also re-parent elements by dragging them within the **Layers** panel. **Example Hierarchy in the Layers Panel:** ```text theme={null} - Container (Hero Section) - Text (Title: "Welcome to Saasio") - Text (Paragraph: "Build amazing apps...") - Button (Call to Action: "Get Started") ``` # Displaying Dynamic Data (Data Binding) Source: https://docs.saasio.io/editor-essentials/displaying-dynamic-data Learn the magic of data binding. This guide explains how to connect your UI elements to your data sources like state variables, repeating groups, and the current user. *** Imagine you have a label on a box. If you change what's inside the box, you want the label to magically update too. That's exactly what **Data Binding** is. It’s the magic that links your UI elements (the label) to your data (what’s in the box). When your data changes, your UI updates automatically. This is the key to creating an interactive application. *** ## How to Bind Data to an Element You can make almost any element dynamic, from a piece of text to an image source. The process starts in the **Properties Panel**. On the canvas, click on the Text element, Image, or any other element you want to make dynamic. In the **Properties Panel** on the right, find the property that controls the element's content. This is often labeled **"Display"**. By default, it's set to "Static Text". Click the dropdown and change the value from "Static" to the source you want to connect to. The most common choice is **"State Variable"**. A new field will appear. Click it and choose the exact piece of data you want to display from the list. Connecting a text element to a state variable. The element is now "bound" to your data. Whenever that data changes, the element on the screen will change with it, automatically. *** ## The Three Main Data Sources You can bind your UI elements to three primary sources of data. **What it is:** The temporary memory of your page. **When to use it:** This is your go-to source for most dynamic data, like loading indicators, user input from a form, or temporary results from an API call. **What it is:** Data for a single item in a list you are displaying. **When to use it:** When your element is **inside a Repeating Group** (a list). This allows you to display a property from the specific item for that row, like showing the `name` of each `Product` in a product list. **What it is:** The database record for the user who is currently logged into your application. **When to use it:** Anytime you want to display personalized information, like the user's name, email, or profile picture. # Pages & API Routes Source: https://docs.saasio.io/editor-essentials/pageAndApiRoutes Learn how to create the fundamental structure of your Saasio application by adding pages for your user interface and API routes for your backend logic. *** Every application you build in Saasio is composed of two fundamental building blocks. Understanding the distinct role of each is the key to creating a powerful, full-stack application. Pages are what your users see and interact with—like a homepage, a dashboard, or a profile section. They are the visual **front-end** of your application. API Routes are the invisible **backend**. They are server-side endpoints that handle data, process requests, and communicate with your database or other services. *** ## Supported Features: Dynamic & Nested Routing Saasio fully supports the same modern routing patterns found in frameworks like Next.js. This includes: ### **Dynamic Routes** You can define dynamic URL segments using brackets. These work for both **pages** and **API routes**: Examples: * `/user/[id]` * `/blog/[slug]/comments/[comment-id]` * `/product/[product_id]/specs` Dynamic segments let you build flexible, data-driven paths without needing to hardcode all possible variations. ### **Nested Routes** Routes can be deeply nested to match complex application structures. Examples: * `/dashboard/settings/security` * `/user/[id]/posts/[post-id]` * `/shop/[category]/items/[item-id]` Saasio automatically converts your folder-like structure into the correct routing hierarchy and resolves dynamic parameters at runtime. ### Accessing Dynamic Parameters When working with dynamic routes, Saasio automatically extracts the values from the URL and makes them available inside your workflows. You can access them using: * **Page Param** — when you are inside a **Page**\ Use this data source to read the dynamic segment values of the current page URL. * **Route Param** — when you are inside an **API Route**\ Use this data source to read dynamic values coming from the API request path. Examples: * A page at `/user/[id]` accessed via `/user/42`\ → `id` = `"42"` * An API route at `/posts/[post-id]/comments/[comment-id]`\ accessed via `/posts/88/comments/12`\ → `post-id` = `"88"`\ → `comment-id` = `"12"` These dynamic parameters can then be used in any workflow action—such as database queries, conditions, branching logic, or generating API responses. Dynamic parameters work everywhere: regular pages, nested pages, API routes, and deeply nested structures. *** ## Pages: Building Your User Interface Your project's pages form the map of your application. When you create a new project, Saasio automatically includes two essential, non-deletable pages: * **Home (`/`):** The default page users land on when they visit your main URL. * **404 (`/404`):** The "Not Found" page shown when a user tries to access a URL that doesn't exist. Here’s the step-by-step process for adding new pages to your application. In the left panel of the Visual Editor, ensure the **Pages** tab is selected. This area lists all the pages in your project. Click the **`+`** icon at the top of the pages list. This action opens a prompt to name your new page. Provide a clear, simple name for your page. This name will automatically become its URL path. For example: * A page named `pricing` will be accessible at `your-domain.com/pricing`. * A page named `about-us` will be accessible at `your-domain.com/about-us`. Creating a new page in the Saasio editor by providing a name. Once created, you can click on any page in the list to load it onto the central canvas and begin designing its layout and content. *** ## API Routes: Powering Your Backend API Routes are your application's connection to the server. They are essential for any task that requires secure or private logic, such as fetching user data, processing a payment, or connecting to another company's API. Here's how to create one. In the left panel, switch from "Pages" to the **API Routes** tab. Click the **`+`** icon to open the API route configuration dialog. Every API route needs two critical pieces of information: * **Path Name:** The unique URL for your endpoint (e.g., `/users`, `/products`). This is the address that your application (or other services) will call. * **Method:** The HTTP method the route will respond to. This defines the type of action the route performs: * `GET`: **Retrieve Data.** * `POST`: **Create New Data.** * `PUT`: **Update Existing Data.** * `DELETE`: **Remove Data.** Configuring a new API Route with a path and HTTP method. ### Building API Route Logic After creating an API Route, you must define what it does. Select the new route, then click the **Logic** tab in the top navigation bar. This opens the workflow editor for that route. **Critical Rule: Always End Your Workflow with "Route Send Response"** An API route's only job is to receive a request and send a response. The **`Route send response`** action is how you send that response. * Every path in your workflow **must** end with this action. * Both branches of any **Conditioner** must end with a separate `Route send response`. Failing to do so will cause the client request to hang and eventually time out. # Creating Reusable Components Source: https://docs.saasio.io/editor-essentials/reusable-components Learn how to build your own reusable UI components with props and local state, allowing you to create a scalable and maintainable design system for your application. *** As you build your application, you'll often find yourself recreating the same set of UI elements. Imagine building a standardized user profile card with an image, a name, and a button. Instead of rebuilding this card on every page, you can create a single **Reusable Component**. **What is a Reusable Component?** Think of it as your own custom-built LEGO brick. You design and build it once, and then you can use that exact same brick anywhere in your application. If you update the master brick, every copy of it updates automatically. *** ## The Anatomy of a Reusable Component Just like Functions, Reusable Components have their own internal logic and data, which makes them incredibly powerful. *** **Think of these as the "customizable parts" of your LEGO brick.** Props allow you to pass data *into* your component from the outside. For a user profile card, you would have props for the `userImage`, `userName`, and `profileLink`, so each card can display different information while looking the same. *** **Think of these as the component's own private memory.** Local States are variables that exist only within the component. For example, a "Show More" button inside your component could use a local `isExpanded` state to track whether it's open or closed, without affecting anything outside the component. *** ## How to Create a Reusable Component Let's walk through creating a simple "User Profile Card" component. In the **Left Panel** of the Visual Editor, click on the **Components** tab. Click the **`+`** icon to create a new component. Give it a descriptive name, like `UserProfileCard`. This will open a dedicated canvas just for designing this component. With your new component open, you can define its inputs and internal memory in the properties panel. - **To add a Prop:** Define its name (e.g., `userName`) and its data type. - **To add a Local State:** Define its name and data type, just like a normal page state. On the component's canvas, build its visual layout using standard elements. For our `UserProfileCard`, you would add an Image, a Text element, and a Button. Now, **bind these elements to your props**. For example: * Bind the **Image** element's `Source` to the `userImage` prop. * Bind the **Text** element's `Content Source` to the `userName` prop. Your component is now a self-contained, customizable UI element. **Current Limitation:** At this time, you cannot place a reusable component inside itself (this is known as recursion). *** ## How to Use a Reusable Component Once your component is created, using it is easy. 1. Navigate to any page in your application. 2. In the **Left Panel**, switch to the **Components** tab. You will see your newly created `UserProfileCard` in the list. 3. **Drag and drop** your component onto the canvas just like any other UI element. 4. With the component selected, the **Properties Panel** on the right will now show all the **Props** you defined. You can provide static or dynamic data for each prop to customize that specific instance of the component. # Working with State & Conditions Source: https://docs.saasio.io/editor-essentials/state-and-conditions Learn how to make your application interactive by storing data in State variables and using Conditions to dynamically change your UI. *** So far, you've learned how to build a beautiful, static interface. Now, it's time to make it intelligent and interactive. This is done using two core features: **State** and **Conditions**. * **State:** Think of State as the temporary memory for your page. It's where you store data that can change, such as user input, loading indicators, or data fetched from an API. * **Conditions:** Conditions allow your UI elements to react to changes in State. They follow a simple "IF-THEN" logic: IF a certain condition is true, THEN change an element's appearance or value. Mastering these two concepts is the key to building a dynamic application instead of just a static website. *** ## Part 1: Storing Data with State A "state variable" is a container for a piece of data. In Saasio, states are always attached to a specific context: either to a UI element on a **Page** or to the **API Route** itself. ### State on a Page On a page, states are scoped to an element. This means you first select the element that will "own" the state, like a container or the main body of the page. First, select the element that will hold your state variable. For page-wide states, it's common practice to select the top-level **`Body`** element in the **Layers** panel. With the element selected, go to the left panel and click on the **States** tab. This will show a list of all state variables attached to that element. Click the **`+`** icon to open the "Create State" dialog. #### Configuring Your State Variable After clicking the `+` icon, a dialog will appear. Here’s a breakdown of each field you need to configure: A descriptive, user-friendly name for your variable (e.g., `isLoading` or `userList`). The type of data this variable will hold. This can be: * A **simple type** like `Text`, `Number`, or `Boolean`. * A **Data Table** (e.g., `Users`) for storing complex objects. * An **Option Set** for storing a value from a predefined list of choices. Defines if the state holds a single value or a list of values. This defaults to `false`. The initial value when the page loads. This should match the data type (e.g., `false` for a Boolean, or empty for a list). #### Configuration Examples Here’s how you would configure two common types of state variables: This state holds a single `true` or `false` value, perfect for tracking a loading status. * **Name:** `isLoading` * **Data Type:** `Boolean` * **Is List?:** `false` (or leave as default) * **Default Value:** `false` This state holds a list (or array) of complex data from your `Users` table. * **Name:** `userList` * **Data Type:** Select `Users` from the Data Table dropdown. * **Is List?:** `true` * **Default Value:** (leave empty to start with an empty list) ### State in an API Route In an API Route, states are attached globally to the route itself, as there is no UI. The process is simpler: 1. With your API Route open, navigate to the **States** tab in the left panel. 2. Click the **`+`** icon and configure your state variable using the same properties described above. Configuring a new boolean state variable named isLoading. *You have now created a piece of memory for your page or API route that can be updated by workflows and used in your logic.* *** ## Part 2: Displaying State in Your UI (Data Binding) You can "bind" an element's property to a state variable, so it always displays the current value. For example, to display the value of a state variable in a text element: 1. Select the **Text** element on your canvas. 2. In the **Properties Panel** (right side), find the `Display` property. 3. Change its value from "static text" to **"state"**. 4. In the `Data source` field that appears, select your desired state variable. The text element will now dynamically display the current value of that state. *** ## Part 3: Using Conditions to React to State Conditions allow elements to change their appearance or content when your state changes. Let's make a "Submit" button change its text to "Saving..." when our `isLoading` state is `true`. First, select the element you want to change. In this case, it's the **Text element** that is *inside* our button. In the **Left Panel**, navigate to the **Conditions** tab. Click the **`+`** icon to add a new condition. A configuration panel will appear. Here, you define a rule that checks if something is true, and then specify how the element should change in response. #### 1. Define the Condition First, you must define the logic. In the **`condition`** section, build a dynamic expression that results in a `true` or `false` value. *Example: To track a loading state, you would select the `isLoading` state variable and add an operation to check if its value is equal to `true`.* #### 2. Configure the Changes (When the Condition is True) Next, you specify how the element should look or what it should contain **only when the condition you just defined is true**. When the condition is `false`, the element automatically reverts to its original, default properties and styles. You do not need to define a "false" state. You can make two types of changes: * **Change the Primary Value:** In the `properties` section, you can override the element's main content. For our text element, you would set the `values` property to the new text you want: "Saving...". * **Change the Style:** In the `styles` section, you can add one or more CSS style overrides. For example, you could change the button's `backgroundColor` to a muted grey to show it's in a loading state. Now, whenever the `isLoading` state becomes `true`, the button's text will automatically change to "Saving...". When it becomes `false`, it will revert to its original state. # Essential Style Shortcuts Source: https://docs.saasio.io/editor-essentials/style-shortcuts Boost your design speed with Saasio's intuitive style shortcuts. Learn how to apply common CSS properties like width, height, margin, and Flexbox layouts with a few keystrokes. *** To help you design faster and more efficiently, the Saasio editor includes a powerful set of **Style Shortcuts**. These shortcuts allow you to apply common CSS properties directly to a selected element without ever needing to open the style panel. Mastering these shortcuts will dramatically speed up your workflow, especially when creating complex layouts. *** ## How to Use a Style Shortcut 1. **Select an element** on the canvas. 2. Make sure you are not currently editing a text field. 3. Simply **type the shortcut key combination**. The corresponding style will be applied instantly. *** ## Most Useful Style Shortcuts Here is a reference guide to the most common and useful shortcuts available. ### 1. Width & Height Shortcuts Quickly set the dimensions of your elements. | Shortcut | CSS Property Applied | Description | | :------- | :--------------------- | :---------------------------------------------------- | | `w+t` | `width: fit-content;` | Makes the element's width fit its content. | | `w+f` | `width: 100%;` | Makes the element take the full width of its parent. | | `h+t` | `height: fit-content;` | Makes the element's height fit its content. | | `h+f` | `height: 100%;` | Makes the element take the full height of its parent. | ### 2. Margin & Padding Shortcuts Apply consistent spacing to your elements. The number in the shortcut corresponds to the `rem` value. | Shortcut | CSS Property Applied | | :------- | :---------------------------- | | `m+a` | `margin: auto;` | | `p+5` | `padding: 5rem;` | | `m+10` | `margin: 10rem;` | | *etc...* | *(works for various numbers)* | *** ## Flexbox Layout Shortcuts Flexbox is the key to creating modern, responsive layouts. These shortcuts allow you to build complex flex containers in seconds. ### 1. Main Flexbox Setups These are powerful "combo" shortcuts that apply a full set of common Flexbox properties at once. *** This shortcut creates a horizontal, wrapping container. **CSS Applied:** * `display: flex;` * `flex-direction: row;` * `justify-content: space-between;` * `align-items: center;` * `flex-wrap: wrap;` * `gap: 1rem;` *** This shortcut creates a vertical container. **CSS Applied:** * `display: flex;` * `flex-direction: column;` * `justify-content: space-between;` * `align-items: center;` * `flex-wrap: nowrap;` * `gap: 1rem;` ### 2. Flex Direction & Centering Use these to quickly adjust the alignment and direction of items within a flex container. | Shortcut | CSS Property Applied | Description | | :------- | :------------------------- | :----------------------------------- | | `f+x` | `display: flex;` | The foundation for all flex layouts. | | `f+r` | `flex-direction: row;` | Arranges items horizontally. | | `f+c` | `flex-direction: column;` | Arranges items vertically. | | `j+c` | `justify-content: center;` | Centers items along the main axis. | | `i+c` | `align-items: center;` | Centers items along the cross axis. | **Tip:** To perfectly center items both horizontally and vertically, select your container and use three shortcuts in a row: `f+x`, `j+c`, and `i+c`. # Styling & Responsive Design Source: https://docs.saasio.io/editor-essentials/styling-and-responsive-design Learn how to use the Properties Panel to style your elements and create a fully responsive design that looks great on desktop, tablet, and mobile devices. *** A professional application needs to look great on every device. In Saasio, you have complete control over the visual appearance of every element, from its colors and fonts to its layout and spacing. This guide covers how to use the **Properties Panel** to style your elements and ensure your design is fully responsive. *** ## The Properties Panel: Your Styling Hub Whenever you select an element on the canvas or in the Layers panel, the **Right Panel** instantly becomes your **Properties Panel**. This is your contextual hub for controlling every aspect of the selected element's design. The Properties Panel on the right side of the Saasio Visual Editor. *** ## Mastering Responsive Design The key to responsive design in Saasio is the set of device tabs at the top of the Properties Panel: **Desktop**, **Tablet**, and **Mobile**. Saasio uses a "Desktop-First" approach. Styles you set on the **Desktop** tab will automatically cascade down to Tablet and Mobile. You can then override specific styles for smaller devices. **Common Workflow:** 1. **Design for Desktop First:** Perfect your layout and styles in the **Desktop** tab. 2. **Adjust for Tablet:** Switch to the **Tablet** tab. Any style you change here will *only* affect the tablet view and smaller. For example, you might reduce the font size or change the layout direction. 3. **Optimize for Mobile:** Switch to the **Mobile** tab. Make final adjustments for the smallest screens, such as increasing button sizes for easier tapping or stacking elements vertically. *** ## Key Styling Properties The Properties Panel is organized into collapsible sections to help you find the setting you need. Control the size and spacing of your element. This includes `Width`, `Height`, `Padding` (inner spacing), and `Margins` (outer spacing). You can also manage complex layouts using `Flexbox` and `Grid` properties. Define the visual look. This section includes `Background Color`, `Text Color`, `Font Size`, `Font Weight`, `Borders`, and `Shadows`. Manage the positioning of elements on the page. You can set an element's position to be `relative`, `absolute`, or `fixed`, and adjust its `z-index` to control its stacking order. Apply visual transformations without affecting the layout, such as `Rotate` and `Scale`. # Transforming Dynamic Data with Operations Source: https://docs.saasio.io/editor-essentials/transforming-data A detailed guide to using Operations. Learn how to format, calculate, filter, and manipulate any data source before it's displayed in your UI. *** Raw data is rarely ready to be shown to a user. A date needs to be formatted, a name needs to be capitalized, or you might need to calculate a final price. In Saasio, the tools you use to perform these changes are called **Operations**. Operations are powerful steps you can add to any dynamic data binding to transform the value before it's displayed on the screen. *** ## How to Add an Operation You can add one or more Operations to any data binding to transform a value before it's displayed. Select a UI element and bind it to a dynamic data source, such as a State Variable or the Current User. Click into the data binding field. This will open the expression editor, where you'll see a "node" representing your starting data. Click the **`+`** icon that appears after the data source node, and select **"Operation"** from the menu. A list of all available Operations will appear, neatly organized by data type. Select the transformation you wish to apply. You can then configure any required parameters for that operation. Adding an Operation to a data binding expression. *** ## Operations by Data Type Here is a reference guide to the most common and useful Operations, organized by the type of data you are working with. Operations for manipulating text values. * **Change Case:** `to upper case` or `to lower case`. Perfect for formatting names and titles. * **Capitalize:** `Capitalize`. Converts the first character of the text to uppercase. * **Join Text (Concat):** `concat`. Appends text to an existing value. Use this to create dynamic sentences (e.g., join "Welcome, " with a user's name). * **Get Length:** `length`. Returns the number of characters in the text. * **Trim Whitespace:** `trim`. Removes any empty spaces from the beginning and end of the text. Operations for calculations and formatting numbers. * **Math Operations:** `+` (add), `-` (subtract), `*` (multiply), `/` (divide). * **Round Numbers:** `round` (to the nearest integer), `floor` (round down), or `ceil` (round up). * **Check if Even/Odd:** `is even` or `is odd`. Returns `true` or `false`. * **Convert to String:** `to string`. Changes a number to text, useful for joining it with other text. Operations for making dates and times user-friendly. * **Format a Date:** `format`. Converts a timestamp into a human-readable format (e.g., `DD/MM/YYYY` or `Month Day, Year`). * **Add/Subtract Time:** `add days`, `subtract hours`, etc. Allows you to perform date calculations, like finding a future subscription renewal date. * **Display Relative Time:** `calculate elapsed / remaining time`. Shows how long ago an event happened (e.g., "2 hours ago"). * **Extract a Component:** `get year`, `get month`, `get day`. Pulls a specific part out of a date value. Operations for manipulating lists of data, like a list of users or products. * **Filter a List:** `filter`. Creates a new, smaller list containing only the items that meet a specific condition you define. * **Transform a List (Map):** `map`. Creates a new list by transforming each item from an original list (e.g., creating a list of emails from a list of user objects). * **Access a Specific Item:** `at`. Gets a single item from a list based on its position (index). The first item is at index `0`. * **Get List Length:** `length`. Returns the number of items in the list. * **Check for an Item:** `includes`. Returns `true` or `false` if the list contains a specific item. Operations for working with a single, complex object, like a record from your `Users` table. **Use Case:** You have a "Current User" object, and you want to display their email address. *** **How it works:** The `Current User` data source is a special object. It contains not only the user's data but also their authentication `status` (`authenticated`, `unauthenticated`, etc.). Because of this, you must first access the nested `user` object before you can see its properties. The process is a simple two-step selection: 1. **Select the `user` Property:** When you first bind to the `Current User`, the dropdown will show its top-level properties. Choose **`user`** from this list. Think of this as opening the folder that contains the user's actual data. 2. **Select the Desired Field:** After you select `user`, a new dropdown will appear, showing all the fields from your `Users` table (like `name`, `email`, `profileImage`). From this second list, you can now select **`email`**. This two-step process allows you to access any field on the user record. **Use Case:** This is your tool for complex data. In some cases, like with data from an external API (e.g., a **Stripe Event**), an object might be so deeply nested that not all of its properties appear in the dropdown. * **How it works:** Use the `Get value from path` operation. Think of it like a treasure map. You provide a "path" using dot notation (e.g., `data.object.customer_email`) that tells Saasio exactly where to find the piece of data you need, no matter how deep it's buried. # Container Element Source: https://docs.saasio.io/elements/container Learn how to use the Container element to group, lay out, and style other elements for building structured and responsive designs. *** The Container element is one of the most important building blocks for creating well-structured and organized layouts. Its primary purpose is to act as a parent that holds, groups, and arranges any child elements you place inside it, such as text, images, buttons, or even other containers. Mastering the container is key to building everything from simple cards to complex, responsive page sections. ### How to Add a Container To add a Container, find the "Container" element in the "Elements" panel on the left and drag it onto your page. You can then drag other elements directly inside the container. ### Layout Properties (Flexbox - Flex) The power of the Container comes from its layout properties, which are based on the modern CSS Flexbox model. These settings, found in the right-side panel, control how child elements are positioned and spaced within the container. These properties control the overall flow of the child elements. **Direction** Determines the primary axis along which children are arranged. * **Row (Horizontal):** Children are placed side-by-side, from left to right. * **Column (Vertical):** Children are stacked on top of each other, from top to bottom. **Wrap** Determines what happens when child elements overflow the container's space. * **No Wrap:** Children will shrink or overflow and will not move to a new line. * **Wrap:** If there isn't enough space, children will wrap onto the next line. These properties align children along the main and cross axes. **Justify Content** Aligns children along the **main axis** (the `Direction` you've set). * `Start`: Groups children at the beginning. * `Center`: Groups children in the middle. * `End`: Groups children at the end. * `Space-Between`: Distributes children evenly, with the first at the start and the last at the end. * `Space-Around`: Distributes children evenly with half-sized spaces at the ends. **Align Items** Aligns children along the **cross axis** (perpendicular to the `Direction`). * `Start`: Aligns children to the beginning of the cross axis. * `Center`: Aligns children to the middle of the cross axis. * `End`: Aligns children to the end of the cross axis. * `Stretch`: Stretches children to fill the container's cross axis. These properties control the space inside and between elements. **Gap** Defines the space **between** each child element inside the container. This is the modern way to create gutters and spacing without using margins. You can set a row and column gap. **Padding** Defines the space **inside** the container, between its border and the child elements it holds. Think of it as the inner margin of the container. ### Dynamic Visibility Just like other elements, you can show or hide a Container based on certain conditions. This is controlled by the **Visibility** property found in the **Custom** section of the right-side panel. Bind this property to any data source that resolves to a boolean (`true` or `false`). The container and all elements inside it will be hidden when the value is `false`. **Example:** To show a user's profile card only after they have logged in, place the card's elements inside a container and set its Visibility if `{{currentUser.status}}` is `authenticated`. ### Styling the Container You can fully customize the appearance of the container from the right-side styling panel. Common styling options include: * **Background**: Set a solid color, gradient, or background image. * **Borders**: Control the color, width, and style of the container's borders. * **Border Radius**: Adjust to create rounded corners. * **Shadow**: Apply a box shadow to make the container appear elevated. * **Sizing**: Set the `Width`, `Height`, `Min-Width`, and other sizing properties. # Repeating Group Element Source: https://docs.saasio.io/elements/repeating-group Learn how to dynamically render a list or grid of elements by iterating over an array of data with the Repeating Group. *** The Repeating Group is a powerful element designed to display lists or grids of items dynamically. It works by iterating over an array of data you provide and rendering a template of UI elements for each item in that array. This is the go-to element for creating product lists, user directories, social media feeds, comment sections, and any other interface that displays a collection of repeating data. ### How it Works: The Core Concept A Repeating Group has two main parts: 1. **The Data Source:** An array of data that you want to display (e.g., an array of user objects from an API). 2. **The Item Template:** A single set of UI elements (like an image, text, and a button) that you design *inside* the Repeating Group. The Repeating Group will then automatically create a copy of your Item Template for every single item in the Data Source array. ### How to Add and Configure 1. **Add the Element**: Drag a **Repeating Group** from the "Elements" panel onto your page. 2. **Set the Data Source**: With the Repeating Group selected, go to the **Custom** section in the right-side panel. Set the **Data** property to the array you want to iterate over. This can be bound to an API response, a state variable, or any other data source that returns an array. The data source for a Repeating Group **must** be an array. If the data is not an array, the element will not render anything. 3. **Build the Item Template**: Drag other elements *inside* the Repeating Group. For example, to build a user card, you might drag a Container, an Image, and a Text element inside. You only need to design this template **once**. ### Accessing Iteration Data To make the template dynamic, you need to access the data for the specific item being rendered in each repetition. The Repeating Group makes this data available through two special context variables: The data object for the current item in the iteration. For example, if you are iterating over an array of users, `currentItem` would be the user object for that specific repetition. The index number of the current item in the array, starting from `0`. You can use these variables to bind data to the elements in your template. **Example: Displaying a List of Users** Imagine your data source is an array of user objects like this: ```json theme={null} [ { "name": "Alice", "email": "alice@example.com" }, { "name": "Bob", "email": "bob@example.com" }, { "name": "Charlie", "email": "charlie@example.com" } ] ``` 1. You set this array as the **Data** for the Repeating Group. 2. Inside the Repeating Group, you add a Text element. 3. To display the user's name, you set the Text element's content property to `{{currentItem.name}}`. 4. You add another Text element and set its content to `{{currentItem.email}}`. The Repeating Group will automatically generate three pairs of Text elements, each displaying the name and email from the corresponding user object. ### Layout and Arrangement A Repeating Group acts like a container for its generated items. You can control how the items are arranged using its layout properties (Flexbox) in the right-side panel. * **Direction**: Set to `Column` to create a vertical list, or `Row` to create a horizontal list. * **Wrap**: If `Direction` is `Row`, enable `Wrap` to create a grid that wraps to the next line when items run out of space. * **Gap**: Set the spacing between each repeated item, both vertically and horizontally. * **Alignment**: Use `Justify Content` and `Align Items` to control the position of your items within the Repeating Group container. ### Styling You can style the main Repeating Group container itself with properties like: * **Background**: Set a background color for the entire list area. * **Padding**: Add space around the entire group of items. * **Borders** and **Shadow**: Add borders and shadows to the main container. # Sidebar Element Source: https://docs.saasio.io/elements/sidebar Learn how to create a responsive, collapsible navigation sidebar with custom headers, footers, and a flexible content structure. *** The Sidebar element provides a powerful, out-of-the-box solution for creating the main navigation menu for your application. It supports collapsible groups, icons, separators, and custom components for its header and footer sections. ### How to Add a Sidebar To add a Sidebar, find it in the "Elements" panel and drag it onto your page. It's typically placed on the left side and set to fill the full height of the viewport. ### Configuration All configuration for the sidebar is handled in the **Custom** section of the right-side properties panel. #### Header and Footer You can add a custom header or footer to your sidebar, which is perfect for displaying a logo, a user profile, or action buttons. * **Header Component**: Select a component you have already created to render at the top of the sidebar. * **Footer Component**: Select a component you have already created to render at the bottom of the sidebar. #### Sidebar Content This is the core of your sidebar. The **Content** property accepts an array of objects that defines the links, groups, and separators that make up your navigation. There are three types of objects you can add to this array: 1. **Item**: A single, clickable navigation link. 2. **Group**: A collapsible section that contains other items. 3. **Separator**: A visual dividing line. An `item` object creates a single navigation link. **Type Definition (JSON):** ```json theme={null} { "item": { "title": "string", "url": "string", "iconName": "string (optional, Lucide Icon)", "openInNewTab": "boolean (optional)", "iconStyle": { "key": "value" } } } ``` **Schema Breakdown:** The text label that will be displayed for the link. The URL that the user will be navigated to on click. The name of an icon from the **[Lucide React](https://lucide.dev/icons/)** library to display next to the title. If set to `true`, the link will open in a new browser tab. A style object to apply custom CSS to the icon. A `group` object creates a collapsible section with a label and nested links. **Type Definition (JSON):** ```json theme={null} { "group": { "label": "string", "iconName": "string (optional, Lucide Icon)", "defaultOpen": "boolean (optional)", "iconStyle": { "key": "value" }, "items": [{ "title": "string", "url": "string", "iconName": "string" }], "subItems": [{ "title": "string", "url": "string" }] } } ``` **Schema Breakdown:** The text label for the group header. The name of a **Lucide React** icon to display next to the group label. If set to `true`, the group will be expanded by default when the sidebar loads. An array of `item` objects that can each have their own icons. These are displayed directly under the group label. An array of `item` objects that are displayed as a simpler, nested list under the main `items`. These typically do not have icons. A style object to apply custom CSS to the group's icon. A `separator` object creates a visual dividing line, useful for organizing sections. **Type Definition (JSON):** ```json theme={null} { "separator": { "enabled": true, "style": { "key": "value" } } } ``` **Schema Breakdown:** Must be set to `true` to display the separator. A style object to apply custom CSS to the separator line. ### Complete Content Example Here is an example of a valid JSON object for the **Content** property, demonstrating how these different object types work together. ```json theme={null} [ { "item": { "title": "Home", "url": "#", "iconName": "Home" } }, { "item": { "title": "Users", "url": "#", "iconName": "User" } }, { "separator": { "enabled": true } }, { "group": { "label": "Dashboard", "defaultOpen": true, "iconName": "LayoutDashboard", "subItems": [ { "title": "Analytics", "url": "#" }, { "title": "Reports", "url": "#" } ] } }, { "group": { "label": "Management", "iconName": "Settings", "items": [ { "title": "Products", "url": "#", "iconName": "Box" }, { "title": "Orders", "url": "#", "iconName": "ShoppingCart" } ] } } ] ``` # Table Element: Displaying Your Data in Rows and Columns Source: https://docs.saasio.io/elements/table Learn how to use the Table element in Saasio to organize and display data in a structured and customizable format. *** The Table element is a powerful tool for presenting data in a clear and organized manner within your Saasio application. It allows you to structure information in a grid of rows and columns, making it easy for your users to scan, compare, and understand the data you're presenting. # How to Add a Table To add a Table element to your page, simply drag it from the "UI Libraries" panel. You can find the Table element under the **Shadcn UI** section on the left-hand side of the editor. Screenshot of a table example. # Managing Table Columns You can define the structure and behavior of your table by configuring its columns. This is done in the **Custom** section of the right-side properties panel, using the `columns` property. The `columns` property accepts an array of objects, where each object defines a single column in your table. Here is a basic example of a columns configuration: ```json theme={null} [ { "headerName": "Status" }, { "headerName": "Email", "sortHeader": "asc" }, { "headerName": "Amount" } ] ``` ## Column Properties Explained Each column object follows the `TtableColumn` type definition and can have the following properties: ```typescript theme={null} type TtableColumn = { headerName: string; sortHeader?: "asc" | "desc"; cell?: { reusableComponentName?: string; props?: Record; formatCell?: "decimal" | "currency" | "percent"; currency?: string; }; }; ``` * `headerName` (Required): A string that defines the title displayed in the column's header (Must lowercase). * `sortHeader` (Optional): Enables sorting on this column. You can set the initial sorting direction to `"asc"` for ascending or `"desc"` for descending. When a user clicks the header, the sort order will toggle. * `cell` (Optional): An object that allows you to customize how the data within each cell of the column is rendered and formatted. ## Customizing Cells The `cell` object gives you fine-grained control over the appearance and functionality of your table cells. * **`formatCell`**: Automatically format numerical data. * `"decimal"`: Formats the number as a decimal. * `"currency"`: Formats the number as a currency value. You must also provide the `currency` property. * `"percent"`: Displays the number as a percentage. * **`currency`**: When using `formatCell: "currency"`, you must specify the currency symbol or code (e.g., "USD", "EUR"). * **`reusableComponentName`**: Instead of displaying simple text, you can render a reusable component you've already created within a cell. Simply enter the name of your component as a string. * **`props`**: When using a `reusableComponentName`, you can pass data to it using the `props` object. This allows you to create dynamic and interactive cells. There are two special string values you can use within the `props` object to pass data from the table to your component: * `"[current_cell]"`: This value is replaced with the data for the specific cell being rendered. * `"[current_row]"`: This value is replaced with the entire data object for the row that the cell belongs to. **Example:** Imagine you have a reusable component called "StatusBadge" that accepts a `statusText` prop. You could configure your column like this: ```json theme={null} { "headerName": "Status", "cell": { "reusableComponentName": "StatusBadge", "props": { "statusText": "[current_cell]", "code": 200 } } } ``` In this example, for each row, the "StatusBadge" component will be rendered. The value of the `status` cell for that row will be passed to the component's `statusText` prop. # Accessing Table Data You can access various properties and data sets from your Table element to use in other parts of your application. This is useful for displaying statistics (like the total number of items), building custom external pagination controls, or performing actions on selected rows. To access this data, create a new **Data Source** and configure it as follows: 1. Set the **Data source** type to `Element Value`. 2. In the **Select element** dropdown, choose your table (e.g., `Shadcn-table`). 3. A final dropdown will appear, allowing you to select which specific piece of data you want to access from the table. Screenshot of a table value. ## Available Data Properties Here is a complete list of the data you can retrieve from a Table element. Each property can be accessed via the `Element Value` data source. An array containing all the row data objects in the table, ignoring any filtering or pagination. An array containing the row data objects that are currently visible on the active page. A number representing the total count of rows in the dataset before pagination. The index of the current page, starting from `0`. A number indicating how many rows are displayed per page. The total number of pages calculated from the total rows and the page size. A boolean value that is `true` if a next page exists. This is useful for dynamically enabling or disabling a "Next" button. A boolean value that is `true` if a previous page exists. This is useful for dynamically enabling or disabling a "Previous" button. An array containing the data objects for all rows that the user has selected. This requires row selection to be enabled on the table. An array of all row data objects that match the current filter criteria, ignoring pagination. # Table Actions You can programmatically control the table's state and behavior from any workflow, such as a button click or a page load event. This allows you to build custom controls for filtering, pagination, sorting, and more. To control the table, add an action to your workflow and select **"Trigger Table Action"**. You will then be prompted to choose the target table and the specific action you want to perform. Screenshot of a table value. ## Available Actions You can programmatically control the table from any workflow. Here is a complete list of the actions you can trigger on a Table element, presented as a collapsible list. Clears all filters currently applied to the table. **Parameters:** None Resets the table to its initial state, clearing all filters, sorting, and column order changes. **Parameters:** None Clears the filter that has been applied to a single, specified column. **Parameters:** - `Column` (string): The `headerName` (lowercase) of the column to clear the filter from. Removes all sorting applied to the table's columns. **Parameters:** None Navigates to the next page of the table. **Parameters:** None Navigates to the previous page of the table. **Parameters:** None Resets the order of all columns back to their default state. **Parameters:** None Sets or updates a filter value for a specific column. **Parameters:** - `Column` (string): The `headerName` (lowercase) of the column to filter. - `Filter Value` (any): The value to filter the column by. Moves a column to a specific position (index) in the table. **Parameters:** * `Column` (string): The `headerName` (lowercase) of the column to reorder. * `Index` (number): The new zero-based index for the column. Navigates directly to a specific page in the table. **Parameters:** - `Page Index` (number): The index of the page to navigate to (e.g., `0` for the first page). Changes the number of rows displayed on each page. **Parameters:** - `Page Size` (number): The number of rows to show per page. Applies sorting to a specific column. **Parameters:** - `Column` (string): The `headerName` (lowercase) of the column to sort. - `Descending` (boolean): Set to `true` for descending order or `false` for ascending order. Shows or hides a specific column. **Parameters:** - `Column` (string): The `headerName` (lowercase) of the column to toggle. ## Filtering Examples The `Set Column Filter` action is versatile. The behavior of the filter depends on the `Filter Value` you provide. Here are some common scenarios and how to configure them.

**Use Case:** You have a search input field where users can type to filter a "Name" column.

**Action Configuration:** * **Column:** `Name` * **Filter Value:** Bind this to the value of your search input element. **Behavior:** The table will display only the rows where the "Name" column *contains* the text entered in the search input. This is case-insensitive. For example, typing "john" would match "John Doe" and "Johnson".

**Use Case:** You have two input fields to filter an "Amount" or "Order Date" column for values within a specific range.

To filter a range, you must provide a two-element array `[min, max]` as the `Filter Value`. This works for both numbers and date strings. Filter for amounts of \$100 or more. This sets a minimum value but no maximum. ```json theme={null} [100, undefined] ``` Filter for amounts of \$500 or less. This sets a maximum value but no minimum. ```json theme={null} [undefined, 500] ``` Filter for amounts between $100 and $500. This sets both a minimum and a maximum value. ```json theme={null} [100, 500] ```

**Use Case:** You have a "Reset" button that should only clear the status filter.

**Action Configuration:** * **Action:** `Set Column Filter` * **Column:** `status` * **Filter Value:** Leave the value empty or set it to `undefined`. For a simpler configuration, you can use the dedicated **`Clear Column Filter`** action instead of setting an empty filter value.
# Styling the Table You can customize the appearance of your Table element to match your application's design using the styling options in the right-side properties panel. To ensure that hover effects and other dynamic styles work correctly, you **must** set a **Background Color** for the main Table element itself. If the table's base background color is not set, styles like the **Row Hover Background Color** may not be visible or could appear broken. # Text Element: Headings and Paragraphs Source: https://docs.saasio.io/elements/text Learn how to display static and dynamic text content using the Text element, including paragraphs and headings (H1, H2, etc.). *** The Text element is a fundamental building block for displaying all written content in your application. You can use it to create everything from page titles and section headings (H1, H2, H3, etc.) to descriptive paragraphs and dynamic data-driven values. ### How to Add a Text Element To add a Text element, find it in the "Elements" panel on the left and drag it onto your page. ### Configuring the Text Content The content displayed by the Text element is controlled from the **Custom** section in the right-side properties panel. The most important property is **Display**, which determines the source of your text. The 'Display' property in the Custom section allows you to select the source of the text. There are four ways to display text: This is the most straightforward option. Choose this to type a fixed piece of text directly into the properties panel. The text will not change unless you edit it manually. **Use Case:** Page titles, labels, instructions, or any content that doesn't need to be dynamic. This option binds the Text element to a **State** variable you have created elsewhere in your application. Whenever the value of the state variable changes, the text displayed on the screen will automatically update to reflect the new value. **Use Case:** Displaying a counter, showing the result of a calculation, or reflecting a selection made by the user. This option connects the Text element to the current user's session data. You can select specific attributes to display, such as their name (`currentUser.user.name`), email (`currentUser.user.email`), or check their authentication status (`currentUser.status`). **Use Case:** Greeting a logged-in user with "Welcome, John!", or displaying their email address in a profile section. This provides the most flexibility by allowing you to use data binding to construct the text from any available data source. You can combine static text with dynamic data from states, APIs, or other elements. **Use Case:** Creating complex strings like `You have {{cart.items.length}} items in your cart` or formatting data in a specific way before displaying it. ### Accessing the Element's Value The **Element's Value** for a Text element is its current rendered text content. No matter which display source you use (Static, State, etc.), the final string that appears on the screen is exposed as the element's value. You can access this value from other elements by using the `Element Value` data source and selecting the Text element. **Use Case:** Imagine you have a dynamic text element named `greetingText`. You could create another text element to display its character count with a custom binding like: `Character count: {{greetingText.value.length}}`. ### Element Type (Headings and Paragraphs) For semantic HTML and good SEO, it's important to use the correct HTML tag for your text. In the properties panel, you can select the **HTML Tag** for the element, such as: * **`H1`**: For the main page title (use only one per page). * **`H2`, `H3`**: For section and sub-section headings. * **`P`**: For standard paragraphs of text. ### Styling You can fully customize the appearance of your Text element from the right-side panel. Common styling options include: * **Font**: Set the font family, size, and color. * **Weight**: Choose from options like normal, medium, semi-bold, and bold. * **Alignment**: Align the text to the left, center, right, or justified. * **Decorations**: Add styles like underline or strikethrough.\`\`\` ### Dynamic Visibility You can show or hide a Text element based on certain conditions. This is controlled by the **Visibility** property found in the **Custom** section of the right-side panel. This field accepts a boolean value (`true` or `false`). You can bind this to any data source that resolves to a boolean. When the value is `true`, the element is visible. When it is `false`, the element is hidden. **Example:** To show a success message only after a user has logged in, you could set the Visibility property of your "Login successful!" text element if `{{currentUser.status}}` is `authenticated`. # Introduction Source: https://docs.saasio.io/index Welcome to Saasio Docs – the no-code platform for building, launching, and scaling SaaS with AI-powered speed. # Welcome to Saasio Docs Saasio is a no‑code platform that lets solopreneurs, small teams, and agencies build, launch, and scale SaaS products without writing code. With an intuitive drag‑and‑drop editor, AI assistance, and production‑ready integrations, Saasio turns ideas into working software in days—not months—while keeping costs predictable and low. From UI assembly and workflow automation to data modeling, APIs, payments, and deployment, everything needed to ship and grow a SaaS lives in one place. Whether starting from scratch or systematizing client work, Saasio delivers speed, flexibility, and scalability without the engineering overhead. # Why Saasio? Build and launch SaaS products in days, not months. Skip expensive developers and complex stacks by going no-code. Let the AI agent automate design, workflows, and logic for you. Manage multiple SaaS applications from a single dashboard. Connect Stripe, APIs, authentication, and third-party services easily. # Who Is Saasio For? * **Solopreneurs** – validate and launch ideas quickly. * **Small teams & startups** – develop and scale SaaS cost-effectively. * **Non-technical founders** – build without writing code. * **Agencies & freelancers** – deliver SaaS faster for clients. *** ## 🛠 Core Features Drag-and-drop interface to build fully functional SaaS products without writing code. Automate workflows, generate UI, and speed up development with built-in AI assistance. Access Saasio UI plus Shadcn, Magic UI, and Aceternity UI for modern design components. Create dynamic forms with custom validation powered by Zod and React Hook Form. Set up subscriptions and payments quickly using Stripe. Define API routes and backend logic visually using Hono. Built-in caching and Redis support to ensure fast performance at scale. Add smooth, modern animations with Framer Motion — no code required. Native support for the Vercel AI SDK lets you integrate AI models into your apps effortlessly — from chatbots to intelligent workflows — without extra setup. *** # What's Next? Ready to start your journey? Here’s a recommended path to becoming a Saasio pro. Follow our 5-minute guide to create your account and launch your very first SaaS project. This is the best place to begin. [**Start the Quickstart →**](/getting-started/quickstart) Take a deep dive into the editor's powerful features. Learn about the canvas, panels, and all the tools you'll use to build your app. [**Explore the Visual Editor →**](/visual-editor) *** 💡 Saasio helps you go from **idea → launch → growth** faster than ever.\ Let’s build your SaaS together 🚀 # Full Action Reference Source: https://docs.saasio.io/logic/action-reference A complete reference guide to all available actions in Saasio's workflow editor, including their execution context. *** Actions are the individual steps that make up your application's workflows. Each action is designed to run in a specific context: on the user's browser (**Frontend**) or on the Saasio backend (**Server**). Understanding this is key to building powerful logic. *** These actions control the user's browser and run entirely on the **Frontend**. **Purpose:** Redirects the user to another internal page, with the option to pass data through URL parameters. **Key Parameters:** * `Destination Page`: Select the page you want to send the user to from a dropdown list. * `URL Parameters` (Optional): Define key-value pairs to send to the destination page. This is essential for creating dynamic pages, like loading a specific user's profile (`/profile?id=123`) **Purpose:** Redirects the user to an external URL outside of your application. **Key Parameters:** * `URL`: The full external website address. * `Open In`: Choose whether to open in a `"new"` tab or the `"same"` tab. **Purpose:** Reloads the current page, equivalent to the browser's refresh button. **Purpose:** Navigates the user to the previous page in their browser history. **Purpose:** Navigates the user forward to the next page in their browser's history stack. These actions interact with your project's data stores. They can be triggered from the **Frontend** or **Server** but the core logic is always executed securely on the **Server**. **Purpose:** Adds a new row of data to a specified Data Table. This is the primary way to save new information, such as a new user signing up or a user creating a new post. *** #### **Configuration Steps** 1. **Select Target Table:** Choose the Data Table where the new record should be created (e.g., `Users`). 2. **Define Fields to Create:** Map each column in your table to the value you want to store. The value can be static (e.g., a fixed string) or dynamic (e.g., the input from a form field). *** #### **Handling the Response** This is an asynchronous action. Use state variables to manage the result in your UI: * **Result:** Select a state variable to store the newly created row, including its unique `id` and timestamps. The state's data type **must** match the Data Table (e.g., `Users`). * **Error:** Select a `Text (string)` state variable to hold any error messages if the creation fails. * **Is Loading:** Select a `Boolean` state variable that is `true` while the action is in progress. **Purpose:** Modifies one or more existing rows in a Data Table based on specific criteria. Use this for actions like saving changes to a user's profile. *** #### **Configuration Steps** 1. **Select Target Table:** Choose the Data Table you want to update (e.g., `Users`). 2. **Set Conditions (WHERE Clause):** Define the rules to find the specific row(s) to update. For example, "Find the user where 'ID' is equal to the current user's ID". 3. **Define Fields to Update:** Provide the new values for only the columns you want to change. *** #### **Handling the Response** * **Result:** Select a state variable to store the updated row(s). **Note:** This action always returns a **list (array)** of records, even if only one row was updated. Your state variable must be configured as a `List` of the matching Data Table type. * **Error:** Select a `Text (string)` state variable to hold any error messages. * **Is Loading:** Select a `Boolean` state variable to track the update process. **Purpose:** Permanently removes one or more rows from a Data Table. This action is irreversible and should be used with caution. *** #### **Configuration Steps** 1. **Select Target Table:** Choose the Data Table to delete from. 2. **Set Conditions (WHERE Clause):** Define the rules to find the correct row(s) to delete. If you leave the conditions empty, this action will **delete all rows** in the table. *** #### **Handling the Response** * **Result:** This action **does not return any data**. The rows are permanently deleted. You do not need to set a Result state. * **Error:** Select a `Text (string)` state variable to hold any error messages if the deletion fails. * **Is Loading:** Select a `Boolean` state variable to provide UI feedback while the deletion is in progress. **Purpose:** Executes a command directly on your project's Redis instance for advanced caching and real-time features. *** #### **Configuration Steps** 1. **Select Data Type:** Choose the Redis data type you want to work with (`String`, `Hash`, `List`, `Set`, or `Json`). 2. **Select Redis Command:** A new dropdown will show commands specific to the selected data type (e.g., `Set`, `Get`). 3. **Provide Command Parameters:** Fill in the required inputs for the command, such as the `key` and `value`. *** #### **Handling the Response** * **Result:** Select a state variable to store the output of the command. The state's data type must match the command's expected output. * **Error:** Select a `Text (string)` state variable to hold any error messages. * **Is Loading:** Select a `Boolean` state variable to track the command's progress. These actions manage data within the application's state or the browser's local storage. **Purpose:** Updates the value of a state variable. This action can be used on both the **Frontend** (to update the UI) and the **Server** (in API Routes, to pass data between actions). **Key Parameters:** * `Target State`: The state variable you want to update. * `New Value`: The new value to be stored. **Purpose:** Stores a key-value pair in the user's browser, which persists after they close the page. This is a **Frontend-only** action. **Key Parameter:** * `Items to Store`: Define the `key` and `value` for each item to store. **Purpose:** Adds or updates query parameters in the page's URL without reloading. This is a **Frontend-only** action. These actions handle user authentication. **Purpose:** Authenticates a user with their email and password. **Purpose:** Creates a new user account and automatically logs them in. **Purpose:** Ends the current user's session and logs them out. Actions that directly manipulate elements on the page and run exclusively on the **Frontend**. **Purpose:** Changes the visibility of a specific UI element. **Key Parameter:** * `Target Element`: The element to show, hide, or toggle. **Purpose:** Smoothly scrolls the page to bring a specific element into view. **Key Parameter:** * `Target Element`: The element to scroll to. **Purpose:** Dynamically updates the title of the current page in the browser tab. **Key Parameter:** * `New Title`: The new string for the page title. The Conditioner can be used to control the flow of logic in any workflow. **Purpose:** Evaluates a condition and then runs different sequences of actions. This can be used on both the **Frontend** and **Server**. **Key Parameters:** * `Condition to Check`: A dynamic expression that must resolve to `true` or `false`. * `Actions if True` / `Actions if False`: The workflows to run for each outcome. A collection of powerful actions for handling files, sending emails, validating data, and adding custom logic to your workflows. **Purpose:** Sends an email using a pre-configured email template. **Key Parameters:** * `Template`: Select one of your pre-designed Email Templates. * `Display Name`: The name of the sender that the recipient will see. * `Send To`: The recipient's email address. * `Preview`: The dynamic data to inject into your email template (e.g., a user's name or an order number). *** #### **Handling the Response** * `Error`: Select a `Text` state variable to hold any error messages if the email fails to send. * `Is loading`: Select a `Boolean` state variable to track the sending process. **Purpose:** Checks if a piece of data matches a pre-defined structure or rules (a Zod Schema). This action can be used on both the **Frontend** and **Server**. **Key Parameters:** * `Schema`: Select the Zod Schema you want to validate against. * `Data`: The data object you want to validate. * `Is Valid Data`: Select a `Boolean` state variable that will be set to `true` if the data is valid, or `false` if it is not. *** #### **Handling the Response** * `Error`: Select a `Text` state variable to store any specific validation error messages. * `Is loading`: Select a `Boolean` state variable to track the validation process. **Powered by Zod:** Data validation in Saasio is powered by [Zod V4](https://zod.dev/), allowing for robust and type-safe schema definitions. **Purpose:** Displays a small, temporary notification message (a "toast") to the user. This is a **Frontend-only** action. **Key Parameters:** * `Toast Type`: The style of the notification: `Success`, `Error`, or `Info`. * `Toast Position`: Where on the screen the toast should appear. * `Message`: The text content to display in the toast. * `Properties`: Advanced JSON configuration for things like `duration` (in milliseconds). This action uses the [react-hot-toast](https://react-hot-toast.com/) library. **Purpose:** Executes a custom function that you've defined in the function section under the Data tab. This can be used on both the **Frontend** and **Server**. **Key Parameters:** * `Function`: Select the pre-defined function you want to run from the dropdown. * `Result`: Select a state variable to store the output or return value of the function. **Purpose:** Copies a specified piece of text or image to the user's clipboard. This is a **Frontend-only** action. **Purpose:** Prompts the user's browser to download a file. This is a **Frontend-only** action. **Key Parameters:** * `File format`: The format of your source data (e.g., `text`, `base64`, `blob`). * `File data`: The actual content of the file you want to make downloadable. * `File MIME type`: The type of the file, which tells the browser how to handle it (e.g., `application/pdf`, `image/png`). * `File name`: The desired name for the downloaded file. **Purpose:** Converts an array of data into a CSV file and prompts the user to download it. This is a **Frontend-only** action. **Key Parameters:** * `File name`: The desired name for the downloaded `.csv` file (the extension is added automatically). * `CSV data`: The array of objects that you want to convert to CSV format. **Purpose:** Logs a value to the browser's developer console. Essential for debugging and a **Frontend-only** action. **Purpose:** Switches the application's visual theme (e.g., from light to dark mode). This is a **Frontend-only** action. **Purpose:** Pauses the workflow for a specified amount of time before executing the next action. This can be used on both the **Frontend** and **Server**. **Key Parameter:** * `Wait (seconds)`: The number of seconds the workflow should pause before continuing. Actions for connecting to external APIs and leveraging powerful, natively integrated AI models. **Purpose:** Executes a pre-configured API endpoint from your "Data" tab. This is your primary tool for interacting with any third-party API. **Purpose:** Provides a suite of actions that connect directly to generative AI models, powered by the [Vercel AI SDK](https://ai-sdk.dev/). These actions allow you to build powerful AI-driven features directly into your workflows. *** #### **Available AI SDK Actions:** * **Generate Object:** Sends a prompt and receives a structured JSON object from an AI model. * **Generate Text:** Sends a prompt and receives a block of text in response. * **Stream Object:** Sends a prompt and receives a structured JSON object back as a real-time stream. * **Stream Text:** Sends a prompt and receives a text response as a real-time stream, perfect for chatbot-style "typing" effects. * **Generate Image:** Sends a prompt to an image generation model and receives an image. * **Generate Speech:** Converts a piece of text into spoken audio. * **Transcribe:** Converts an audio file into a text transcript. * **Embedding:** Converts text into a vector embedding for use in AI-powered search or similarity tasks. Actions for sending tracking events to analytics and marketing platforms. These are **Frontend-only** actions. **Purpose:** Pushes a custom event to the Google Tag Manager data layer, allowing you to trigger tags. **Purpose:** Sends event data directly to Google Analytics (GA4) for tracking user interactions and conversions. # Introduction to Workflows Source: https://docs.saasio.io/logic/introduction-to-workflows Learn the fundamentals of Saasio's Logic system. Understand how to use triggers and actions to create powerful, interactive workflows for your application. *** You've designed a beautiful user interface. Now, it's time to make it work. Workflows are the engine of your application—they are the logic that runs when a user interacts with your UI. Whether you're submitting a form, navigating to a new page, or fetching data from an API, everything is handled by a workflow. *** ## The Core Concepts Every workflow in Saasio is built on three simple concepts: This is the event that starts the workflow. Most commonly, it's a user interaction, like **clicking a button**, **submitting a form**, or a **page finishing loading**. An action is a single, specific operation. It's one step in your workflow, such as **"Go to page"**, **"Create new data"**, or **"Set data in state"**. A workflow is simply a sequence of one or more actions that are executed in order when a trigger occurs. *** ## Building Your First Workflow: Page Navigation Let's create the most common workflow: making a button navigate to another page when clicked. On the canvas, select the element that will trigger the workflow. In this case, it's our **Button** element. A selected element will have a blue outline. With the button selected, click on the **Logic** tab in the top navigation bar. This opens the workflow editor for the selected element's default trigger (for a button, the trigger is "On Click"). We want to navigate, so: * Open the **Navigation** category. * Select the **"Go to page..."** action. * A configuration panel will appear. In the `Select a page` field, select your desired destination page from the dropdown. Configuring a 'Go to page' action in the Logic tab. **That's it!** Now, when a user clicks that button in your live application, the "Go to page" action will run, and they will be redirected. You can continue to add more actions to this workflow to create more complex sequences. # Connecting to External APIs Source: https://docs.saasio.io/managing-data/api-calls Learn how to use the API Calls tab to configure and manage connections to any external or third-party API, from OpenAI to Stripe. *** The true power of a SaaS application comes from its ability to connect to other services. The **API Calls** tab is where you build these connections. Whether you want to fetch data from a weather service, send a message to Slack, process a payment with Stripe, or even call one of your own Saasio-built API Routes, you must first configure the connection here. Once an API call is configured, it becomes a reusable action that you can trigger from any workflow in your application. *** ## Structure: Folders and Endpoints For better organization, the API Calls section uses a two-level structure: * **API Folders:** These are groups to organize your API calls. For example, you might create a "Stripe" folder to hold all your Stripe-related calls. * **API Endpoints:** These are the individual configurations for each specific API call, like "Create a Payment" or "Get Customer Details". *** ## How to Configure a New API Call Let's walk through the process of setting up a new API call. In the top navigation bar, click the **Data** tab, then select the `API Calls` sub-tab. First, create a folder to house your API calls. Give it a logical name, like `OpenAI` or `Stripe`. Inside your new folder, click the `+ Add new call` button. This will open the main configuration panel for your new API endpoint. This is the most important step. You need to provide all the details about the API you are trying to connect to. ### Key Configuration Fields Here are the essential properties you need to set up for a new API endpoint. This defines the endpoint your application will call. * **Method:** Choose the HTTP method (`GET`, `POST`, `PUT`, `DELETE`). * **URL:** Paste the full URL of the endpoint. For dynamic parts that will change with each call (like an ID), use square brackets: `.../users/[userId]`. Headers are used for sending metadata with your request. The most common use case is for **authentication**. * **Key:** The name of the header, e.g., `Authorization`. * **Value:** The value for the header. **Security Tip:** For API keys and secret tokens, **always** use an Environment Variable. For example, set the `Authorization` header's value to `Bearer `. The `Body` contains the data you are sending *to* the API. This is typically used with `POST`, `PUT`, and `DELETE` requests. Saasio supports several body types: * **JSON:** The most common format. You can define a JSON structure and use angle brackets `<...>` for dynamic values that will be provided by your workflow. * **Form Data:** For sending `multipart/form-data`, often used when uploading files. * **Raw:** For sending plain text, XML, or even a file directly. Tell Saasio what kind of data you expect the API to send back. This ensures Saasio can correctly process the response. Common types include `JSON`, `Text`, `Image`, or `File`. ## Using Your API Call in a Workflow Once you've saved your API endpoint, it's ready to be used. 1. Go to the **Logic** tab for any element. 2. Add the `Make an API call` action. 3. In the action's configuration, you will be able to select the API Folder and the specific API Endpoint you just created. 4. If your API call has dynamic parameters (like `[userId]` in the URL or `` in the body), you will be prompted to provide a value for each one. The workflow will then execute the call, and you can save the response to a state variable to display in your UI. # Creating and Using Functions Source: https://docs.saasio.io/managing-data/creating-functions Learn how to create reusable workflows with Functions. Define a sequence of actions once and trigger it from anywhere in your application, on either the client or server. *** As your application grows, you might find yourself building the same sequence of actions over and over again. For example, the logic to "add an item to the shopping cart and update inventory" might be needed in multiple places. Instead of rebuilding this logic every time, you can create a **Function**. **What is a Function?** A Function is a reusable workflow. It's like a recipe: you define the steps (actions) once, give it a name, and then you can "cook" that recipe (trigger the function) from anywhere in your application. *** ## The Anatomy of a Function Every function you build in Saasio is like a mini-program with three core parts: the inputs it receives, the memory it uses, and the instructions it follows.
**Think of these as the "ingredients" for your recipe.**
Props are how you pass dynamic data *into* your function when you call it. They make your function reusable. For example, a `SendWelcomeEmail` function needs a `userEmail` prop so it knows who to send the email to. Without props, it could only ever send an email to the same hardcoded address.
**Think of these as your "mixing bowls".**
Local States are temporary variables that exist *only* while the function is running. They are perfect for storing intermediate results or complex calculations inside your function without cluttering your main page's state. Once the function finishes, this internal memory is cleared.
**This is the "recipe" itself.**
The workflow is the sequence of actions that the function will execute. This logic can read the values passed in via **Props** and can read or write to its own **Local States**. The final result of these instructions can then be returned.
*** ## The Two Environments: Client vs. Server The most important decision you will make when creating a function is where it will run. This choice has significant implications for what the function can do. *** **Runs in the user's browser.** Client-side functions are for reusing UI-related tasks. * **Use Cases:** Creating a multi-step "Show Notification" sequence, a custom "Form Reset" workflow, any set of actions that manipulate elements on the page or interact with server. *** **Runs on the Saasio backend.** Server-side functions are for creating your own reusable backend logic. **They can be securely called from your frontend workflows.** * **Use Cases:** A function to `CreateUserProfile` that performs multiple database actions, a function to `ProcessPayment` that calls the Stripe API, or any logic that requires security. * **Cannot do:** Directly interact with the user's page, UI elements or Browser API like `local storage`. *** ## How to Create a Function In the top navigation bar, click the **Data** tab, then select the **"Functions"** sub-tab. * **Name:** Give your function a clear, descriptive name (e.g., `AddToCart` or `SendWelcomeEmail`). * **Environment:** Choose either **`Server`** or **`Client`** from the dropdown. This decision cannot be changed later. After creating the function, you will be taken to the familiar workflow editor. Here, you can add and configure a sequence of actions that make up your function's logic. *** ## Returning Data from a Function Functions can also return a result to the workflow that called them. This is especially powerful for functions that fetch or calculate data. To do this, the **last action** in your function's workflow must be the **"Return function result"** action. * **Go to your function's workflow.** * Add the **"Return function result"** action. * In the `value` field, provide the data you want to send back (e.g., the result of a database query, or a simple success message). *** ## How to Use a Function Once your function is created, you can run it from any other workflow in your application using a specific action. 1. Open the workflow editor for any trigger (e.g., a button's "On Click"). 2. Add the **"Trigger Function"** action. 3. In the action's configuration, select the function you want to run. 4. **To get the result:** If your function returns a value, you can use the `Result` field in the "Trigger Function" action to save the returned data directly into a state variable. Using the 'Trigger Function' action and saving its result to a state. This powerful pattern allows you to keep your frontend logic clean while executing complex, secure operations on the backend, all within a single workflow. # Creating Data Tables Source: https://docs.saasio.io/managing-data/data-tables Learn how to define the core data structure of your application using Data Tables. This is the foundation for storing and managing all your app's information. *** Every powerful application is built on a solid data structure. In Saasio, the foundation of your data is built in **Data Tables**. Think of a Data Table as a spreadsheet or a database table. It's where you define the "shape" of the information you need to store. For example, a `Users` table would define that every user must have an `email` (Text), a `password` (Text), and a `lastLogin` (Date). *** ## What is a Data Table? A Data Table is a blueprint for a specific type of data in your app. It's made up of **fields** (columns) where each field has a specific **data type**. **Important:** The "Data Tables" tab is for defining the **structure** of your data. The actual data records (rows) are viewed and managed in the **"App Data"** tab. *** ## The Two Roles of a Data Table In Saasio, Data Tables can serve two distinct but related purposes. This is the most common use. You create a table like `Products` or `Users` to be your actual database. You will use workflows to **create, read, update, and delete** rows in these tables. This is a more advanced concept. You can create a table that **will not store any data itself**. Instead, its only job is to act as a **reusable blueprint** or a "shape" for your data. You can then reference this shape in your State variables or API responses to ensure your data is always structured correctly. When you create a new table, you will see an option to define its role. *** ## Creating Your First Data Table Let's create a simple `Products` table that will define the structure for products in an e-commerce store. In the top navigation bar of the editor, click on the **Data** tab. You will land on the **Data Tables** sub-tab by default. Enter a name for your new table in the input field (e.g., `Products`) and click the **"Create"** button. Your new table will appear in the list. With your `Products` table selected, you can now add fields to it. Click the **"+ Add new field"** button. For each field, you need to define: * **Field Name:** A descriptive name for the column (e.g., `productName`, `price`, `inStock`). * **Data Type:** The type of data this field will hold. *** ## Understanding Field Data Types Choosing the correct data type is essential for a well-structured app. Here are some of the most common types: For storing plain text, like names, descriptions, or emails. For storing numerical values, like prices, quantities, or ratings. For storing a simple `true` or `false` value, like `inStock` or `isVerified`. For storing a specific date and time, like `createdAt` or `lastLogin`. This is a powerful feature that allows you to link tables together. For example, a `Products` table could have a `seller` field that is a relation to your `Users` table. Check this box if a field should hold a list (array) of values instead of a single value. For example, a `productImages` field could be a `List` of `Images`. **Example `Products` Table Structure:** * `productName` (Text) * `description` (Text) * `price` (Number) * `inStock` (Boolean) * `images` (Image, List) * `seller` (Relation to `Users` Table) # Using Environment Variables Source: https://docs.saasio.io/managing-data/environment-variables Learn how to securely manage API keys and secret tokens for both the AI SDK (automatic) and custom API Calls (manual). *** When you connect your Saasio application to external services like OpenAI or Stripe, you'll need to use **API keys** or **secret tokens**. These are like passwords for your application, and they must be kept private. **Environment Variables** are the correct and secure way to store this sensitive information. **Never** paste your secret API keys directly into an action's configuration. Always use an Environment Variable instead to avoid a major security risk. *** ## What is an Environment Variable? An Environment Variable is a key-value pair that is stored securely on the Saasio backend, not in the public-facing editor. You create a variable with a specific name (the key) and store your secret token as its value. When your application runs a server-side action, Saasio uses these variables to authenticate with external services. This means your secrets are never exposed in your application's code or in the browser. ### Security and Encryption To ensure the highest level of security, **all Environment Variable values are encrypted at rest**. This practice prevents any possibility of a leak and ensures that only your application's backend processes can access the sensitive data. *** ## How to Create an Environment Variable The creation process is the same for all types of secrets. In the top navigation bar, click the **Data** tab, then select the `Environment Variables` sub-tab. Click the `+ Add new variable` button. Two input fields will appear. * **Name (Key):** Enter the name for your variable. **This is case-sensitive and must be exact.** * **Value:** Carefully paste your secret API key or token into this field. Click the `Save` button. Your variable is now securely stored. *** ## How to Use Your Variables How you use a variable depends on the action. **This is the easy way.** For integrated services like the AI SDK, Saasio automatically looks for specific, pre-defined variable names. **You do not need to reference the variable in the action itself.** Simply create the variable with the correct name, and Saasio handles the rest automatically. **Example:** * To use OpenAI, create a variable named exactly `OPENAI_API_KEY`. * Now, any "AI SDK" action that uses OpenAI will work automatically, without you needing to add the key anywhere else. **This is for everything else.** When you configure your own API endpoints in the "API Calls" tab, you need to tell Saasio where to use your secret. You do this by referencing the variable's name inside angle brackets `<...>` in the URL or Headers section. **Example:** * You create a variable named `MY_STRIPE_KEY`. * In your "Make an API call" action, you would set the `Authorization` header to `Bearer `. * Saasio will replace `` with your secret value when the call is made. # Using Option Sets Source: https://docs.saasio.io/managing-data/option-sets Learn how to create and manage reusable lists of choices with Option Sets to ensure data consistency in dropdowns and Data Tables. *** An **Option Set** is a predefined, reusable list of text choices. Think of it as a master list that you can refer to anywhere in your application. For example, instead of manually typing "Free," "Basic," and "Pro" in every dropdown menu for a subscription plan, you can create a single "SubscriptionPlans" Option Set. *** ## Why Use Option Sets? Using Option Sets is a best practice that saves time and prevents errors. Prevent typos and variations by using the same master list everywhere. This ensures that a value is always "Pro" and never accidentally "pro" or "professional". If you need to change or add an option (e.g., adding an "Enterprise" plan), you only have to do it in one place. Every UI element using that set will update automatically. *** ## How to Create an Option Set In the top navigation bar, click the **Data** tab, then select the `Option Sets` sub-tab. Enter a descriptive name for your set (e.g., `SubscriptionPlans`) and click `Create`. With your new set selected, you can add the individual text choices to the list. For our example, you would add "Free", "Basic", and "Pro". *** ## How to Use an Option Set Once created, you can use your Option Set in two primary places: * **In Data Tables:** When creating a field in a Data Table, you can set its **Data Type** to be one of your Option Sets. This restricts the data for that field to only the choices you've defined, ensuring data integrity. # Injecting Custom Scripts Source: https://docs.saasio.io/managing-data/scripts Learn how to use the Scripts tab to inject third-party JavaScript snippets for analytics, live chat widgets, and other services. *** The **Scripts** tab is an advanced feature that allows you to inject custom JavaScript snippets directly into the `` or `` tags of your application's pages. This feature is powerful but should be used with care, as poorly written or untrustworthy scripts can negatively affect your application's performance and security. **Primary Use Case:** This feature is intended for integrating trusted, third-party services that require you to paste a script tag into your site. Common examples include: - Analytics tools (like Hotjar or Mixpanel) - Live chat widgets (like Intercom or Crisp) - Advertising or marketing tracking pixels *** ## How to Add a Script In the top navigation bar, click the **Data** tab, then select the `Scripts` sub-tab. Click to create a new script. You will be presented with a text editor and a location setting. Carefully paste the JavaScript code provided by the third-party service into the text editor. Select whether the script should be injected into the **``** or the **``** of your page. Always follow the instructions provided by the third-party service. * **``:** Use for scripts that need to load before your page content is visible (e.g., analytics, font loaders, or style modifiers). * **``:** Use for scripts that can load after your page content is visible (e.g., chat widgets or other non-essential tools). Save your script. It will now be included on every page of your live, published application. # Creating Zod Schemas Source: https://docs.saasio.io/managing-data/zod-schema Learn how to use [Zod](https://zod.dev/) Schemas to define the structure and validation rules for your application's data, ensuring consistency across forms, workflows, and AI tools. *** Think of a [Zod](https://zod.dev/) Schema as a **rulebook for your data**. It's where you define exactly what a piece of information should look like. For example, a "Sign Up" schema could have these rules: * An `email` field must be provided, and it must look like a real email address. * A `password` field must be provided, and it must be at least 8 characters long. By creating these rulebooks, you ensure that the data flowing through your application is always clean, correct, and secure. Saasio uses the powerful **Zod v4** library to make this possible without writing any code. *** ## Why Use Zod Schemas? Zod Schemas are a versatile tool that you will use in many different parts of your application. This is the most common use case. By linking a Form element to a Zod Schema, you automatically get data structure and validation. The form will know which fields are required, what type of data to expect, and will even show error messages for you. Using the `Validate Data` action, you can check if any piece of data (e.g., from an API call) matches your schema's rules before you try to save it to your database, ensuring data integrity. When working with the [AI SDK](https://ai-sdk.dev/), you can provide a Zod Schema to an AI model. This forces the AI to return its response in a perfectly structured JSON format that matches your rules, making AI outputs reliable and easy to work with. *** ## Creating a Zod Schema Let's create a simple `SignUpSchema` to define the rules for a user registration form. In the top navigation bar, click the **Data** tab, then select the **"Zod Schemas"** sub-tab. Enter a name for your schema (e.g., `SignUpSchema`) and click **"Create"**. With your new schema selected, click **"+ Add new field"**. For each field, you must define its rules in the properties panel. ### Field Validation Rules For each field in your schema, you can define: * **Field Name:** The name of the property (e.g., `email`). * **Data Type:** The type of data, such as `Text`, `Number`, or `Boolean`. * **Validation Rules:** A set of rules to enforce, such as: * **Required:** Is this field mandatory? * **Minimum / Maximum Length:** For text fields. * **Email Format:** Checks if the text is a valid email address. * **Custom Error Message:** You can write your own error message that will be shown to the user if their input is invalid. **Example: `SignUpSchema` Structure** Here is how you would configure the fields for our `SignUpSchema`. | Field Name | Data Type | Key Validation Rules | Error Message Example | | :--------- | :-------- | :-------------------------- | :------------------------------------ | | `name` | `Text` | Required, Minimum length: 2 | "Name must be at least 2 characters." | | `email` | `Email` | Required | "Please enter a valid email address." | | `password` | `Text` | Required, Minimum length: 8 | "Password must be 8+ characters." | # Quickstart Source: https://docs.saasio.io/quickstart Your 5-minute guide to signing up, logging in, and creating your first SaaS project in Saasio. *** Welcome to Saasio! This guide will get you from zero to your first project in under five minutes. We'll walk through creating your account and launching your first application. Let's begin. *** ## Part 1: Create Your Account Your Saasio account is your personal workspace. Navigate to [**Saasio.io**](https://saasio.io) and click the **Sign Up** button. You will need to provide your email address and a secure password. Check your inbox for a verification email. Click the link inside to confirm your account and protect its security. If you don't see it, check your spam folder. Once verified, log in with your new credentials. You will be directed to your main **Projects Dashboard**, which is the central hub for all your applications. *** ## Part 2: Create Your First Project A **Project** in Saasio is a complete, standalone SaaS application. It comes with its own database, pages, and backend logic. On your Projects Dashboard, click the **"Create Project"** button. This will open the project setup screen. You will be asked for three key details: * **Project Name:** A public name for your app (e.g., "AI Content Writer"). * **Project Subdomain:** A unique URL for your app (e.g., `ai-writer.saasio.io`). * **Description:** A short, internal summary of your project. Create Project Modal in Saasio Click **Create Project**. Your project will be set up instantly, and you'll be taken directly into the Saasio Visual Editor. Congratulations! You are now inside your first project and ready to start building. # Best Practices for Building Secure SaaS Apps Source: https://docs.saasio.io/tutorials/building-secure-apps Learn the essential principles and best practices for building a secure and trustworthy SaaS application on the Saasio platform. *** Building a great SaaS product isn't just about features and design—it's about earning and keeping your users' trust. A secure application is the foundation of that trust. While Saasio provides a secure infrastructure and powerful tools out of the box, the security of your final application also depends on the choices you make as a builder. This guide outlines the key principles and best practices you should follow. *** ## The Shared Responsibility Model Think of security as a partnership. * **Saasio's Responsibility (The Secure Foundation):** We provide a secure, robust infrastructure. We handle things like server security, database protection, encrypting your secrets, and providing secure, pre-built actions. * **Your Responsibility (The Secure Blueprint):** You are responsible for using Saasio's tools correctly to build secure application logic. This includes protecting your pages, validating data, and managing user permissions. By working together, we can create a secure environment for your users. *** ## What Saasio Secures for You Automatically You get a huge head start on security just by building on Saasio. Here are some of the things we handle for you: All your sensitive logic, like database queries and API calls, runs on the Saasio backend, never in the user's browser where it could be exposed. Any value you save as an **Environment Variable** is encrypted at rest and can never be viewed again in the editor, ensuring your API keys and tokens are safe. Our built-in **Account Actions** (Login, Sign-up, Logout) use industry-standard security practices to manage user sessions and protect passwords. Actions like **"Get Stripe event"** automatically handle complex security checks (like signature verification) to ensure that incoming webhooks are legitimate and not malicious. *** ## Your Checklist: 6 Best Practices for Secure Building As a builder, your primary job is to ensure your application's logic is secure. Here are the most important practices to follow. This is the most important rule of web security. Any logic that runs on the **Client** (in the user's browser) can be viewed. Therefore, you must never place secret API keys or tokens in a frontend workflow. * **Do This (Secure):** Store your key in an **Environment Variable** and use it in a **Server-side Function** or **API Route**. * **Never Do This (Insecure):** Paste a secret key directly into a "Make an API call" action that is part of a frontend workflow. Not all users should see all pages. Always add a security checkpoint to pages that contain sensitive information. * **Best Practice:** On any protected page (like a dashboard), use the **"On Page Load"** workflow to check if the `Current User's status` is `unauthenticated` and redirect them to the login page. (See the Authentication Tutorial). Just because a user is logged in doesn't mean they should be able to do everything. Use roles to control access to features. * **Best Practice:** Add a `role` field to your `Users` table (e.g., "user" vs. "admin"). Before running a sensitive action (like deleting a user), use a **Conditioner** to check if the `Current User's role` is equal to `admin`. Any action that modifies important data, grants permissions, or processes payments **must** be executed on the server. * **Best Practice:** Instead of putting complex logic in a button's "On Click" workflow, create a **Server-side Function** for the task (e.g., `ProcessPayment`). The button's only job is to trigger that secure function. This prevents users from manipulating the logic from their browser. Frontend validation (using a Zod Schema on a Form) is great for user experience, but it can be bypassed. True security comes from server-side validation. * **Best Practice:** In any API Route or Server Function that receives data, your **first step** should be to use the **"Validate Data"** action to check the incoming data against a Zod Schema before you save it to the database. Don't show buttons or links for actions a user isn't allowed to perform. * **Best Practice:** Use a **Condition** on your UI elements to control their visibility. For example, an "Admin Panel" button should have a condition to hide it (`display: none`) if the `Current User's role` is not `admin`. By following these fundamental principles, you can leverage Saasio's secure foundation to build a robust, professional, and trustworthy SaaS product. # Tutorial: Creating a Form with Validation Source: https://docs.saasio.io/tutorials/creating-a-validated-form A step-by-step guide to building a contact form that uses a Zod Schema for real-time validation and provides clear feedback to the user. *** Forms are the primary way you'll collect information from your users. In this tutorial, we'll build a complete contact form that includes real-time validation to ensure the data is correct before it's submitted. We will cover: 1. **Creating a Zod Schema** to define our data rules. 2. **Building the Form UI** with input fields and a submit button. 3. **Linking the Form** to our Zod Schema. 4. **Creating a Workflow** to handle the submission and show feedback. *** ## Part 1: Define the Rules with a Zod Schema First, we need to create the "rulebook" for our form data. Go to the **Data** tab in the top navigation bar, then select the **"Zod Schemas"** sub-tab. Create a new schema and name it `ContactFormSchema`. Add the following fields and validation rules to your schema. This will define the structure and requirements for our form. | Field Name | Data Type | Key Validation Rules | Error Message Example | | :--------- | :-------- | :--------------------------- | :------------------------------------ | | `name` | `Text` | Required, Minimum length: 2 | "Name must be at least 2 characters." | | `email` | `Email` | Required | "Please enter a valid email address." | | `message` | `Text` | Required, Minimum length: 10 | "Message must be 10+ characters." | *** ## Part 2: Build the Form UI Now, let's create the visual form on our page. 1. **Add a Form Element:** From the **Elements** tab, drag a **Form** element onto your canvas. This will act as the container for our inputs. 2. **Add Input Fields and Error Text:** For each field in our schema (`name`, `email`, `message`), you will add two elements *inside* the Form container: * An **Input** element where the user will type. * A **Text** element placed directly below the input. This is where the validation error message will appear. You can style it to be red to make it stand out. 3. **Add a Submit Button:** Drag a **Button** *inside* the Form container. In its properties, set its **`type`** to **`Submit`**. This is a special type that tells the button to trigger the form's own workflow. 4. **Add a Text Element for Feedback:** Place a Text element somewhere near the form. We will use this to show a "Thank you!" message. *** ## Part 3: Link the Form to the Schema This is the magic step where we connect our UI to our rules. Click on the main **Form** element on your canvas (the container). In the **Properties Panel** on the right, find the property for **`Schema`**. Click the dropdown and select your newly created `ContactFormSchema`. Now, we need to tell the form how each input and error message corresponds to a rule in our schema. * **For the "Name" Input:** 1. Select the `name` **Input** element. In its properties, set the **`Name`** to `name`. 2. Select the `name` **Text** element below it. In its properties, find the **`Form error message`** and select the `name` field from the schema. * **For the "Email" Input:** 1. Select the `email` **Input** element and set its **`Name`** to `email`. 2. Select the `email` **Text** element and set its **`Form error message`** to the `email` field. * **For the "Message" Input:** 1. Select the `message` **Input** element and set its **`Name`** to `message`. 2. Select the `message` **Text** element and set its **`Form error message`** to the `message` field. *** ## Part 4: Create the Submission Workflow Finally, we need to tell the form what to do when the user clicks the "Submit" button or press the "Enter" key and the data is valid. 1. **Select the Form Element:** Select the main **Form** container again. 2. **Open the Logic Tab:** Go to the **Logic** tab in the top navigation bar. Because it's a form, it has a special trigger called **"On Submit"**. This workflow will *only* run if all the data passes the schema's validation rules. 3. **Add Your Actions:** In the "On Submit" workflow, you can now add the actions to process the valid data. For this tutorial, let's do two things: * Add a **"Create new data"** action to save the form's data to a `Submissions` Data Table. You can access the form's data using the `Data Submitted from Form` variable. * Add a **"Show / Hide element"** action to display the "Thank you!" message you created earlier. Now you have a fully functional and validated form! When a user fills it out, they will get instant feedback if they make a mistake, and the data will only be processed once everything is correct. # Tutorial: Creating a Tabbed Layout Source: https://docs.saasio.io/tutorials/creating-tabbed-layouts Learn how to build a dynamic tabbed interface. This guide covers using local state to track the active tab and Conditions to show and hide content panels. *** Tabbed layouts are a fantastic way to organize content on a single page, such as a user's "Settings" or "Profile" page. Instead of showing everything at once, you can break it down into logical sections, and the user can switch between them without reloading the page. In this tutorial, we will build a simple three-tab interface ("Profile", "Billing", and "Security"). This is a perfect exercise for mastering **local state** and **conditions**. *** ## Part 1: Set Up the State The entire tab system is controlled by a single state variable that keeps track of which tab is currently active. 1. **Navigate to your page** (e.g., a new `settings` page). 2. Select the `Page body` and go to the **States** tab. 3. Create a new state variable: * **Name:** `activeTab` * **Data Type:** `Text` * **Default Value:** `profile` (This will be the tab that is visible when the page first loads). *** ## Part 2: Build the UI Our UI will have two main parts: the tab buttons that the user clicks, and the content panels that are shown or hidden. * Add three **Button** or **Text** elements to your page. Label them "Profile", "Billing", and "Security". These will be our clickable tabs. * Below the tab buttons, add three **Container** elements. * **Inside the first container:** Add some text or inputs related to "Profile". * **Inside the second container:** Add content related to "Billing". * **Inside the third container:** Add content related to "Security". Each container is a "panel" that corresponds to one of our tabs. *** ## Part 3: Create the Workflows Now, we need to create the logic that updates our `activeTab` state when a user clicks a button. * **For the "Profile" Button:** * Create an **"On Click"** workflow. * Add a **"Set data in state"** action. * **Target State:** `activeTab`. * **New Value:** Set it to the static text `profile`. * **For the "Billing" Button:** * Create an **"On Click"** workflow. * Add a **"Set data in state"** action that sets `activeTab` to `billing`. * **For the "Security" Button:** * Create an **"On Click"** workflow. * Add a **"Set data in state"** action that sets `activeTab` to `security`. Now, whenever a user clicks a button, our `activeTab` state will update to reflect their choice. *** ## Part 4: Conditionally Show the Content Panels This is the final step where we connect the state to the UI's visibility. We will use conditions to show only the panel that matches the active tab. * Select the **Container** for the "Profile" content. * Go to the **Conditions** tab in the **Left Panel**. * Add a new condition: * **Condition Logic:** Check if the `activeTab` state variable `is not equal to` `profile`. * **Style Change:** In the `styles` section, set the `display` property to `none`. * Repeat the process for the other two panels: * For the **Billing Container**, add a condition to hide it (`display: none`) if `activeTab` is not equal to `billing`. * For the **Security Container**, add a condition to hide it (`display: none`) if `activeTab` is not equal to `security`. **How it Works:** By default, all three panels will try to be visible. Our conditions act as "hiding rules". The "Profile" panel will only be hidden if the active tab *isn't* "profile"—meaning it will be visible only when it *is* "profile". *** ## Bonus: Styling the Active Tab To give the user better visual feedback, you can also use conditions to change the style of the currently active tab button. * Select the "Profile" button. * Go to its **Conditions** tab. * Add a condition: IF `activeTab` is equal to `profile`, THEN change its `backgroundColor` or `font-weight`. * Repeat for the "Billing" and "Security" buttons. You have now built a clean, fully functional tabbed interface, a common and essential component of modern web applications. # Tutorial: Creating, Updating, and Deleting Data (CRUD) Source: https://docs.saasio.io/tutorials/crud-operations Learn how to build a complete data management system. This guide covers the essential C.R.U.D. (Create, Read, Update, Delete) operations for your database. *** You've learned how to read and display data (the "R" in C.R.U.D.). Now it's time to master the other three essential operations: **Create, Update, and Delete**. In this tutorial, we will build a complete management system for our blog posts, allowing us to add new posts, edit existing ones, and remove them. *** ## Prerequisites This tutorial builds directly on the previous two. You must have already completed: 1. **"Displaying a Dynamic List of Data"**: You need a page showing a list of all your posts. 2. **"Displaying Single Record Details"**: You need a `post-detail` page that can load a single post based on a URL parameter. *** ## Part 1: CREATE - Building a "New Post" Form First, let's create a way to add new posts to our database. #### **Create the Zod Schema** Before we build the forms, let's create a single "rulebook" for our post data. This ensures that new posts and edited posts follow the same validation rules. * Go to **Data → Zod Schemas**. * Create a new schema named `PostSchema`. * Add the following two fields: | Field Name | Data Type | Key Validation Rules | | :--------- | :-------- | :--------------------------- | | `title` | `Text` | Required, Minimum length: 5 | | `content` | `Text` | Required, Minimum length: 20 | * Create a new page named `new-post`. * Add a **Form** element to the canvas. * Inside the form: * Add an **Input** element. In its properties, set its **Name** to `title`. * Add another **Input** (or a Text Area) and set its **Name** to `content`. * Add a **Submit Button** labeled "Create Post". * **Crucially, select the main Form element and in its Properties Panel, link its `Schema` to your `PostSchema`**. This automatically enables validation. * Select the **Form** element and open its **"On Submit"** workflow from the **Logic** tab. - Add the **"Create new data"** action. * **Target Table:** Select your `Posts` Data Table. * **Fields to Create:** We need to map our form inputs to the database columns. * For the `title` field, bind its value to the `Data submitted from form`'s `title` property. * For the `content` field, bind its value to the `Data submitted from form`'s `content` property. * After the "Create new data" action, add a **"Go to page..."** action. * Set the destination to your main blog list page. This way, the user sees their newly created post in the list immediately after creating it. *** ## Part 2: UPDATE - Building an "Edit Post" Form Next, let's allow users to edit a post on the `post-detail` page. * Go to your `post-detail` page. * Add a **Form** element. **Link its `Schema` to your `PostSchema`**. * Inside the form, add two **Input** elements. * Set the first input's **Name** to `title`. * Set the second input's **Name** to `content`. * **Pre-fill the form:** Bind the *default value* of each input to the `currentPost` state. * Bind the title input's value to `currentPost.title`. * Bind the content input's value to `currentPost.content`. * Add a **Submit Button** inside the form labeled "Save Changes". * Select the "Save Changes" button and open its **"On Click"** workflow. * Add the **"Update existing data"** action. * **Target Table:** Select your `Posts` Data Table. * **Conditions (WHERE Clause):** Tell Saasio *which* post to update. Add a condition where `_id` is equal to the `currentPost._id`. * **Fields to Update:** Map the database columns to the *current values from the form*. * Set the `title` field to the `Data submitted from form`'s `title` property. * Set the `content` field to the `Data submitted from form`'s `content` property. * After the update action, add a **"Show toast message"** action with a `Success` type and a message like "Post updated successfully!". *** ## Part 3: DELETE - Adding a "Delete Post" Button Finally, let's add the ability to remove a post. * On your `post-detail` page, add a new **Button** labeled "Delete Post". It's good practice to style it differently (e.g., with a red color) to indicate a destructive action. * Select the "Delete Post" button and open its **"On Click"** workflow. - Add the **"Delete data"** action. * **Target Table:** Select your `Posts` Data Table. - **Conditions (WHERE Clause):** This is the most important step. You must specify which post to delete. Add a condition where `_id` is equal to the `_id` of the `currentPost` state variable. Never leave the conditions empty on a Delete action, as this would delete **all posts** in your table. * After a post is deleted, the detail page is no longer valid. You must redirect the user. * Add a **"Go to page..."** action and send the user back to your main blog list page. Congratulations! You have now implemented the full suite of CRUD operations. Your users can create, read, update, and delete data, forming the core of a fully functional web application. # Tutorial: Displaying a Dynamic List of Data Source: https://docs.saasio.io/tutorials/displaying-dynamic-lists Learn how to fetch a list of records from your database and display it on your page using a Repeating Group, including loading and empty states. *** One of the most common features in any application is a list of items fetched from a database—a list of products, a feed of social media posts, or a directory of users. In this tutorial, we'll build a complete, dynamic list of blog posts directly from our database. We will cover: 1. **Creating a Data Table** for our posts. 2. **Setting up State** to store the list and loading status. 3. **Building the "On Page Load" Workflow** to fetch the data. 4. **Using a Repeating Group** to display the data in the UI. 5. **Handling Loading and Empty States** for a great user experience. *** ## Part 1: Set Up Your Data First, we need some data to display. * Go to **Data → Data Tables**. * Create a new table named `Posts`. * Give it two fields: `title` (Text) and `content` (Text). * Go to the **Data → App Data** tab. * Select your `Posts` table. * Manually add 2-3 sample blog posts by clicking "+ Add new row" and filling in the titles and content. This will give us something to see when we fetch the data. *** ## Part 2: Set Up the Page and States Now, let's prepare the page where the list will be displayed. 1. **Create State Variables:** On your page, select the `Body` and go to the **States** tab. Create: * `postList` (Data Type: `Posts` Data Table, **Is List?: `true`**) * `isLoading` (Data Type: `Boolean`, Default Value: `true` — We want to show loading immediately). 2. **Add UI Elements:** * Drag a **Repeating Group** element onto your canvas. This is the special element for displaying lists. * *Inside* the Repeating Group, place two **Text** elements: one for the post title and one for the content. The Repeating Group will automatically create a copy of these for each item in our list. *** ## Part 3: Fetch Data on Page Load This is where we connect everything. We will build a workflow that runs the moment the page loads to fetch our data directly from the database. * Select your page in the **Pages** tab or **Canvas**. * Go to the **Logic** tab to open the **"On Page Load"** workflow. * Choose the **"Set data in state"** action. * **Target State:** Select your`postList` state variable. This is the key step. We will configure the action to get its value directly from the database. * In the **New Value** field, click to open the expression editor. * From the data source list, select **"Get data from DB"**. * A new configuration panel will appear: * **Table to search:** Select your `Posts` Data Table. * **Filters (Optional):** You can add conditions here to filter the results (e.g., find posts where `isPublished` is `true`). For this tutorial, we will leave this empty to fetch *all* posts. * **Sort by (Optional):** You can choose a field to sort the results by, like `createdAt`. This "Get data from DB" option is a secure, server-side data fetch that runs and returns the data directly to your state variable. * **After** the "Set data in state" action, add a second **"Set data in state"** action. * **Target State:** Select your `isLoading` state. * **New Value:** Set it to `false`. This ensures the loading state is turned off only after the data has been successfully fetched. *** ## Part 4: Display the Data in the UI Now we just need to bind our UI elements to the data. 1. **Bind the Repeating Group:** Select the main **Repeating Group** element. In its Properties Panel, bind its data source to your `postList` **state variable**. 2. **Bind the Internal Elements:** * Select the **title Text element** *inside* the Repeating Group. * Set its **Content Source** to **"Repeating Group Item"** and its **Value** to the `title` property. * Do the same for the **content Text element**, setting its **Value** to the `content` property. *** ## Part 5: Polishing with Loading and Empty States For a professional feel, you should give the user feedback. * **Loading State:** Add a Text element that says "Loading posts...". Use a **Condition** to make it visible only when the `isLoading` state is `true`. * **Empty State:** Add another Text element that says "No posts found.". Use a **Condition** to make it visible only when `isLoading` is `false` **AND** the `length` of the `postList` state is `0`. You have now built a complete, production-ready feature for displaying a dynamic list of data directly from your database! # Tutorial: Displaying Single Record Details Source: https://docs.saasio.io/tutorials/displaying-single-record-details Learn how to build a dynamic detail page. Pass an ID via URL parameters to fetch and display a single, specific record from your database. *** In the last tutorial, we learned how to display a list of all blog posts. Now, we'll build the next logical feature: a "detail page" that shows the full content of a single post when a user clicks on it from the list. This is a fundamental pattern in web development. We will cover: 1. **Creating a Dynamic Detail Page** that can display any post. 2. **Passing a Record's ID** from the list page to the detail page using URL parameters. 3. **Fetching and Displaying** the data for that single, specific record. *** ## Prerequisites This tutorial assumes you have already completed the **"Displaying a Dynamic List of Data"** tutorial. You should have: * A `Posts` Data Table with some sample data. * A page that displays a list of these posts in a Repeating Group. *** ## Part 1: Create the Detail Page First, we need a new page that will act as our template for displaying any single post. 1. **Create a New Page:** Go to the **Pages** tab and create a new page named `post-detail`. 2. **Set Up State:** On this new page, select the `Body` and create two state variables: * `currentPost` (Data Type: `Posts` Data Table, **Is List?: `false`**) * `isLoading` (Data Type: `Boolean`, Default Value: `true`) 3. **Add UI Elements:** Add two **Text** elements to the canvas: one large one for the title and a smaller one for the content. We will bind these later. *** ## Part 2: Link the List Page to the Detail Page Now, we need to make each item in our post list clickable. When a user clicks, we need to send them to the `post-detail` page and tell that page *which post* to load. We do this by passing the post's unique `id` in the URL. * Go back to your main post list page. * In your **Repeating Group**, make sure the elements for each item (the title and content text) are wrapped in a **Container**. This container will be our clickable link. * Select the **Container** inside your Repeating Group. - Open the **Logic** tab to create an **"On Click"** workflow. - Add the **"Go to page..."** action. * **Destination Page:** Select your `post-detail` page. * **URL Parameters:** This is the key step. * Click **"+ Add parameter"**. * For the **Key**, type `postId`. * For the **Value**, we need to get the ID of the specific post in that row. Bind the value to the **`Repeating Group Item`'s `_id` property**. Now, when a user clicks on the first post, they will be navigated to a URL like `/post-detail?postId=123xyz`. *** ## Part 3: Fetch a Single Record on the Detail Page The `post-detail` page now knows which post to load by looking at the `postId` in the URL. Let's build the workflow to fetch its data when the page loads. * Go to your `post-detail` page. * Open its **"On Page Load"** workflow from the **Logic** tab. * Add a **"Set data in state"** action. - **Target State:** Select your `currentPost` state variable. In the **New Value** field, we will build a two-step expression to first fetch the data and then select the single item from the result. #### **Step 3a: Fetch the Data from the Database** * In the expression editor, select the **"Get data from DB"** option. * **Table to search:** Select your `Posts` Data Table. * **Filters:** This is where we tell Saasio which post to find. * Set up the filter rule: `_id` (the record's unique ID) `is equal to` the value from the **URL parameter** named `postId`. Even when you filter by a unique ID, the "Get data from DB" source **always returns a list (array)** of items. In this case, it will be a list containing just one item. Our next step is to extract that single item. #### **Step 3b: Select the First Item from the List** * After the "Get data from DB" node in your expression, click the `+` icon and add an **Operation**. * Choose the **`at`** operation from the Array (List) transformations. * For the **index**, provide a static `Number` with a value of `0`. Your final expression will first fetch a list containing the single post that matches the ID, and then the `at index 0` operation will pull that single post object out of the list. * **After** the data fetch action, add another **"Set data in state"** action. * Set your `isLoading` state to `false`. *** ## Part 4: Display the Single Record's Data Finally, let's bind our UI elements to the `currentPost` state. 1. Select the **title Text element**. Bind its **Content Source** to the `currentPost` state variable and select `title` property. 2. Select the **content Text element**. Bind its **Content Source** to the `currentPost` state variable and select `content` property. You have now built a complete list-detail pattern! Users can browse a full list of items, click on any one of them, and be taken to a dynamic page that loads and displays the details for that specific record. # Tutorial: Implementing Pagination on a List Source: https://docs.saasio.io/tutorials/implementing-pagination Learn how to add pagination to your dynamic lists to improve performance and user experience. This guide covers managing page state, offset/limit data fetching, and disabling buttons. *** When your application has hundreds of records, fetching and displaying all of them at once is slow. The solution is **Pagination**—breaking the data into smaller, numbered pages. In this tutorial, we will add "Next" and "Previous" buttons to our blog post list and display the current page information. *** ## Prerequisites You must have a page that displays a list of posts from your database. For this tutorial to be effective, make sure you have at least 10-15 sample posts in your `Posts` table. *** ## Part 1: Set Up the State for Pagination We need new state variables to keep track of our pagination status. 1. **Navigate to your post list page.** 2. Select the `Body` and go to the **States** tab. 3. **Add or update** the following state variables: * `postList` (Data Type: `Posts` Data Table, Is List?: `true`) * `isLoading` (Data Type: `Boolean`, Default Value: `true`) * `currentPage` (Data Type: `Number`, Default Value: `1`) * `itemsPerPage` (Data Type: `Number`, Default Value: `5`) * `totalPosts` (Data Type: `Number`, Default Value: `0`) *** ## Part 2: Build the 'On Page Load' Workflow This workflow will run when the page first loads, fetching the initial page of data and the total number of posts. * Open the **"On Page Load"** workflow from the **Logic** tab. * Add a **"Set data in state"** action. * **Target State:** `postList`. * **New Value:** In the expression editor, select **"Get data from DB"**. * **Table:** `Posts`. * **Limit:** Bind this to your `itemsPerPage` state. * **Offset:** Build the expression `(currentPage - 1) * itemsPerPage`. This will correctly be `0` on the first page load. * Add a second **"Set data in state"** action. * **Target State:** `totalPosts`. * **New Value:** We will fetch the entire list and then get its length. 1. In the expression editor, select **"Get data from DB"**. 2. **Table:** `Posts`. **Do not set a Limit or Offset.** 3. After the "Get data from DB" node, add an **Operation** and choose the **`length`** operation. * Add a third **"Set data in state"** action at the very end. * **Target State:** `isLoading`. * **New Value:** Set it to `false`. *** ## Part 3: Build the Pagination Buttons (The Basic Way) Now, let's create the buttons to change pages. 1. **Add the Buttons:** On your page, add two **Buttons**: "Previous" and "Next". 2. **"Next" Button Workflow:** * Create an **"On Click"** workflow. * **Action 1:** `Set data in state` to update `currentPage` to `currentPage + 1`. * **Action 2:** `Set data in state` to update `postList`. **You must copy and paste the entire data fetch expression** from Part 2 here. 3. **"Previous" Button Workflow:** * Do the same, but set `currentPage` to `currentPage - 1`, followed by the same copied data fetch expression. This works, but notice the repetition? You've used the exact same data fetch logic in three different places (On Page Load, Next Button, Previous Button). If you need to change how you fetch data, you'll have to update it in all three places. Let's fix this with a Function. *** ## Part 4: Refactor with a Reusable Function (The Better Way) Instead of putting our data-fetching logic directly on the page, we will create a self-contained "recipe" for it. * Go to **Data → Functions**. * Create a new **Client-side** Function named `fetchPosts`. Our function needs to know which page to fetch and how many items to get per page. Add two **Props**: * `pageToFetch` (Data Type: `Number`) * `limit` (Data Type: `Number`) The function's job is to fetch the data and then **return it**. 1. In the workflow editor for your `fetchPosts` function, add the **"Return function result"** action. This must be the only action. 2. In the `value` field of this action, build your data-fetching expression: * Select **"Get data from DB"**. * **Table:** `Posts`. * **Limit:** Bind this to the `limit` **Prop**. * **Offset:** Build the expression `(pageToFetch - 1) * limit`. **Crucially, use the `pageToFetch` and `limit` Props from the function itself.** This function is now a perfect "black box". It accepts two inputs (`pageToFetch` and `limit`) and returns a list of posts. It has no knowledge of the page it's being called from. Now, we can use our powerful new function to simplify our page logic. * Open the **"On Page Load"** workflow for your post list page. * **Action 1: Trigger the Function to Fetch Posts** * Add the **"Trigger Function"** action. * **Function:** `fetchPosts`. * **Props:** * `pageToFetch`: Pass the `currentPage` state (which is `1`). * `limit`: Pass the `itemsPerPage` state. * **Result:** In the `Result` tab of the action, save the returned list of posts into your `postList` state variable. * **Action 2: Get the Total Count** * Add a `Set data in state` action to set the `totalPosts` state by fetching all posts and applying a `length` operation. * **Action 3: Update Loading State** * Add a `Set data in state` action to set `isLoading` to `false`. * Select the "Next" button and create an **"On Click"** workflow. * **Action 1: Increment the Page Number** * Add a `Set data in state` action to update `currentPage` to `currentPage + 1`. * **Action 2: Trigger the Function Again** * Add a **"Trigger Function"** action. * **Function:** `fetchPosts`. * **Props:** Pass the `currentPage` and `itemsPerPage` states. **This will use the newly updated page number.** * **Result:** Save the returned value into the `postList` state. This will update the UI. * Create the workflow for the "Previous" button. It will be the same as the "Next" button, but you will decrement the `currentPage` state in the first action. *** ## Part 4: Add Conditional Logic for a Professional UI Finally, let's disable the buttons when they can't be used and show the user where they are. * **Disable the "Previous" Button:** * Select the "Previous" button. In its **Properties Panel**, bind the `Disabled` property to the expression: `currentPage <= 1`. * **Disable the "Next" Button:** * Select the "Next" button. Bind its `Disabled` property to the expression: `currentPage * itemsPerPage >= totalPosts`. * **Display Page Info:** * Add a **Text** element and bind its content to a dynamic expression to show something like: `Page [currentPage] of [ceil(totalPosts / itemsPerPage)]`. You have now successfully implemented a complete, high-performance pagination system using the correct workflows! # Tutorial: Displaying Real-Time API Data Source: https://docs.saasio.io/tutorials/real-time-api-data A step-by-step guide to fetching, streaming, and displaying data from a public API in real-time, complete with loading states and error handling. *** In this tutorial, we will build a complete, real-world feature from start to finish. We'll create a "Joke Generator" that calls a public API to fetch a random joke and **streams** the result back to our UI in real-time, like a chatbot. We will cover: 1. **Configuring an API Call** to connect to an external service. 2. **Setting up State** to manage the joke data, loading status, and potential errors. 3. **Building the UI** to display the data and provide user feedback. 4. **Creating a Workflow** that ties everything together. *** ## Prerequisites: Setting Up Your Page Before we begin, you need a page with a few basic elements. 1. **Create Three State Variables:** Select the `Body` of your page and go to the **States** tab. Create the following: * `jokeText` (Data Type: `Text`, Default Value: "Click the button to get a joke!") * `isLoading` (Data Type: `Boolean`, Default Value: `false`) * `errorMessage` (Data Type: `Text`, Default Value: leave empty) 2. **Add the UI Elements:** On your canvas, add: * A **Button** with the text "Get New Joke". * A **Text Element** to display the joke. **Bind its Content Source to the `jokeText` state variable.** * A **Text Element** for errors (e.g., "Oops! Something went wrong."). **Use a Condition** to make this element visible only when the `errorMessage` state is not empty. *** ## Part 1: Configure the API Call First, we need to teach Saasio how to talk to the external joke API. In the top navigation bar, go to **Data → API Calls**. Create a new folder to keep things organized. Let's call it `Joke API`. Inside the folder, click **"+ Add new call"** and name it `Get Random Joke`. This will open the configuration panel. Fill in the details for the public API. For this tutorial, we'll use a simple joke API. * **Method:** `GET` * **URL:** `https://official-joke-api.appspot.com/random_joke` * **Response Type:** `JSON` This is a simple public API, so no **Headers** or **Body** are needed. *** ## Part 2: Build the Workflow Now we'll create the logic that runs when the user clicks our button. This workflow will handle the API call, update our UI with the result, and manage any potential errors. Go back to your page and select the "Get New Joke" button. Then, click the **Logic** tab in the top navigation bar. Before we fetch a new joke, it's good practice to clear out any old data. * Click **"+ Add Action"** and choose **"Set data in state"**. * **Target State:** Select your `errorMessage` state. * **New Value:** Leave the value empty to clear any previous error messages. * Add a second **"Set data in state"** action, select `jokeText`, and set its value to "Fetching new joke..." to provide instant feedback. This is the core of our workflow. * Click **"+ Add Action"** again and choose **"Make an API call"**. * **API Folder:** Select `Joke API`. * **API Endpoint:** Select `Get Random Joke`. Now, we tell Saasio how to automatically manage our state based on the API call's outcome. * **Result:** Select your `jokeText` state variable. The successful response from the API will be stored here. * **Error:** Select your `errorMessage` state. If the API call fails for any reason, the error message will be stored here. * **Is Loading:** Select your `isLoading` state. Saasio will **automatically** set this to `true` when the call starts and back to `false` when it finishes (whether it succeeds or fails). **Transforming the Result:** The API returns a JSON object like `{"setup": "Why did the...", "punchline": "Because..."}`. We need to combine these into one string. In the **Result** field's configuration, you'll build a dynamic expression: 1. Start with the `actionResult` (this represents the data returned from the API). 2. Use the `Get value from path` operation to get the `setup`. 3. Add a `concat` operation to add a space or a new line. 4. Use another `Get value from path` operation on the `actionResult` to get the `punchline`. Your final expression will create a single, complete joke that gets stored in the `jokeText` state. *** ## The Final Result You've now built a complete, end-to-end feature! * When a user clicks the button, the `isLoading` state becomes `true` (you could use this to show a loading spinner). * The API call is made. * If it's successful, the setup and punchline are combined and **streamed** into the `jokeText` state, causing the text on the screen to update in real-time. * If it fails, the `errorMessage` state is populated, causing your error message to appear. * When the call is finished, `isLoading` is automatically set back to `false`. # Tutorial: Sending Transactional Emails Source: https://docs.saasio.io/tutorials/sending-transactional-emails Learn how to send automated emails to your users. This guide covers creating a dynamic email template and triggering it from a secure, server-side Function. *** Automated emails are essential for communicating with your users. Whether you're welcoming a new user, confirming an order, or resetting a password, these "transactional" emails are a core part of any application. In this tutorial, we will build a workflow that automatically sends a "Welcome!" email to a user immediately after they sign up. We will cover: 1. **Creating a dynamic Email Template** with personalized content. 2. **Building a secure, server-side Function** to handle the email sending. 3. **Triggering the Function** from our existing Sign-Up workflow. *** ## Part 1: Create a Dynamic Email Template First, we need to design the email itself. We'll create a template that can be personalized with the new user's name. In the **Left Panel** of the Visual Editor, navigate to the **Email Templates** tab. * Click to create a new template and give it a name, like `WelcomeEmail`. * You will be taken to an editor where you can design your email's subject and body. This is the key step. We need to define "placeholders" for the data that will be different for each user. * **Subject:** Set the subject to something like `Welcome to Our App, !` * **Body:** In the body of the email, you can write your welcome message. Where you want to display the user's name, use the same placeholder syntax: `Hi , we're so glad you've joined.` The `` is a **variable**. We will provide a value for this variable when we send the email. *** ## Part 2: Build a Secure "Send Welcome Email" Function Sending emails must be done on the server. A **Server-side Function** is the perfect, reusable tool for this job. * Go to **Data → Functions**. * Create a new **Server-side** Function and name it `SendWelcomeEmail`. Our function needs to know *who* to send the email to and *what name* to use for personalization. Add two **Props**: * `userEmail` (Data Type: `Text`) * `userName` (Data Type: `Text`) * Open the workflow editor for your `SendWelcomeEmail` function. * Add the **"Send Email"** action. * **Template:** Select your `WelcomeEmail` template from the dropdown. * **Send To:** Bind this to the `userEmail` **Prop** of your function. * **Display Name:** Enter the "From" name for your app (e.g., "The Saasio Team"). * **Preview (Dynamic Data):** This is where we provide the values for our placeholders. * The **key** must match the variable name in your template (`name`). * The **value** should be bound to the `userName` **Prop** of your function. Your secure, reusable function is now ready. It accepts an email and a name, and sends your beautifully designed welcome email. *** ## Part 3: Trigger the Function After Sign-Up The final step is to call our new function from our existing user registration workflow. 1. **Navigate to your `signup` page.** 2. Select the **Sign-Up Form** and open its **"On Submit"** workflow from the **Logic** tab. 3. **Find the successful sign-up path.** Your workflow should have a **"Sign up and Login"** action. This action returns the newly created user object. 4. **Add the "Trigger Function" action** immediately after the "Sign up and Login" action. * **Function:** Select your `SendWelcomeEmail` function. * **Props:** * `userEmail`: Bind this to the `email` from the **Result** of the "Sign up and Login" action. * `userName`: Bind this to the `name` from the **Result** of the "Sign up and Login" action. By placing this in the same workflow, the welcome email is sent instantly and automatically the moment a new user successfully creates their account. You have now built a complete transactional email system, a critical feature for user onboarding and engagement in any SaaS application. You can reuse your `SendWelcomeEmail` function anywhere you need it. # Tutorial: Processing Payments with Stripe Source: https://docs.saasio.io/tutorials/stripe-payments A complete, end-to-end guide to integrating Stripe for payments. Learn to create a checkout session, handle redirects, and securely process webhook events. *** Accepting payments is the core of most SaaS businesses. This tutorial will guide you through a complete, production-ready Stripe integration, covering the entire flow from a user clicking "Subscribe" to securely verifying their payment on the backend. We will build two key pieces of logic: 1. **An API Route** to securely create a Stripe Checkout session. 2. **A Webhook API Route** to listen for and process successful payment events from Stripe. *** ## Prerequisites 1. **Stripe Account:** You need a Stripe account with your API keys (Secret Key and Publishable Key). 2. **Environment Variables:** Securely store your Stripe Secret Key. Go to **Data → Environment Variables** and create a new variable named exactly `STRIPE_SECRET_KEY`. 3. **Webhook Secret:** In your Stripe Dashboard, go to the Webhooks section and create a new endpoint. You will get a "Signing secret". Create another Environment Variable named `STRIPE_WEBHOOK_SECRET` and store this value. *** ## Part 1: Create the "Checkout Session" API Route We can't create a checkout session from the frontend, as it would expose our secret key. We must do it on the server using an API Route. * Go to the **API Routes** tab in the left panel. * Create a new API Route with the following settings: * **Path Name:** `/create-checkout-session` * **Method:** `POST` * With the new API Route selected, open the **Logic** tab. - This workflow will use a special, pre-built **"Stripe Create Checkout Session"** action. - Add this action to your workflow. The action requires details about the product the user is buying. * **Line Items:** This is where you define the price and quantity. You can get this data from the request that your frontend will send. * **Success URL:** The full URL of the page where you want to send the user after a successful payment (e.g., `https://yourapp.com/success`). * **Cancel URL:** The URL where the user should be sent if they cancel the checkout process. * The "Create Checkout Session" action will return a checkout session object from Stripe, which contains a `url`. * Add a **"Route send response"** action as the final step. * Set its value to the **`url`** from the result of the previous action. This sends the unique checkout URL back to the frontend. *** ## Part 2: Trigger the Checkout from the UI Now, let's make a "Subscribe" button on your pricing page that calls this API Route. 1. **Select your "Subscribe" button** and open its **Logic** tab. 2. Add the **"Make an API call"** action. 3. Configure it to call the `/create-checkout-session` endpoint you just made. 4. **Handle the response:** The result of this API call will be the checkout URL. 5. Add a **"Go to external website"** action as the next step. 6. Set its `URL` parameter to be the **result of the "Make an API call" action**. Now, when a user clicks the button, your app will securely create a Stripe session and immediately redirect the user to the Stripe checkout page. *** ## Part 3: Handle Webhook Events from Stripe When a payment is successful, Stripe needs to tell your application. It does this by sending a special message called a **webhook**. We need to create a dedicated API Route to listen for these webhooks and process them securely. * Go to the **API Routes** tab in the left panel. * Create a new API Route with the following settings: * **Path Name:** `/stripe-webhook` * **Method:** `POST` * **Provide API route URL** to Stripe in your Webhook settings in the Stripe Dashboard. You'll need to listen for the `checkout.session.completed` event. * Before building the workflow, you need a place to store the webhook data. * With your new API Route selected, go to the **States** tab. * Create a new state variable. Let's name it `stripeEvent`. For its **Data Type**, you will find a special, pre-built type called **`Stripe Event`**. Select this. * Now, open the **Logic** tab for your new webhook route. * The **very first action** in your workflow must be the special **"Get Stripe event"** action. * **Configure the action:** In the `Set Event` field, select the `stripeEvent` state variable you just created. **Automatic Security:** This single action handles all the complex security for you. It automatically uses your `STRIPE_WEBHOOK_SECRET` and `STRIPE_SECRET_KEY` Environment Variables to verify that the request is genuinely from Stripe and securely parses the event data. A single webhook endpoint receives many types of events. We need to build logic to handle each case we care about. * Add a **"Conditioner"** action after the "Get Stripe event" action. * **Condition:** Check if the **`type`** property of your `stripeEvent` state variable is equal to the text `"checkout.session.completed"`. #### **If the Payment is Successful (Actions if True)** This branch runs when a user successfully pays. 1. **Fulfill the Order:** Add your business logic here. For example, use the customer ID from the `stripeEvent` data to find the user in your database and add an "Update existing data" action to set their subscription status to "active". #### **If the Event is Something Else (Actions if False)** This branch runs for any other event. Here, we'll add another check specifically for failed payments. 1. **Add a Nested Conditioner:** Inside the `Actions if False` branch, add a second **"Conditioner"** action. 2. **Condition:** Configure this inner conditioner to check if the `stripeEvent.type` is equal to `"payment_intent.payment_failed"`. * **If True (Payment Failed):** Add the logic to handle the failure. For example, use the **"Send Email"** action to send a "Payment Failed" email to the customer. You can get the customer's email from the `stripeEvent` data. * **If False (Other Event):** Leave this branch empty. This will handle any other event types that you don't need to act on. Stripe needs to know you've received the event. You **must** end every possible path in your workflow with a **"Route send response"** action. * Add a **"Route send response"** action at the end of the **first `true` branch** (after fulfilling the order). * Add a **"Route send response"** action at the end of the **nested `true` branch** (after sending the failure email). * Add a **"Route send response"** action at the end of the **nested `false` branch**. A common response is a status code `200` with a simple JSON body like `{"received": true}`. This tells Stripe "Thank you, I've handled it" and prevents them from retrying the webhook. Your final workflow structure will look like this: ```text theme={null} - Get Stripe event - Conditioner (is event 'checkout.session.completed'?) - TRUE: - Fulfill Order (update user DB) - Send 200 Response - FALSE: - Conditioner (is event 'payment_intent.payment_failed'?) - TRUE: - Send Failure Email - Send 200 Response - FALSE: - Send 200 Response (to acknowledge other events) ``` # Tutorial: Setting Up User Authentication Source: https://docs.saasio.io/tutorials/user-authentication A complete guide to implementing user sign-up, login, and page protection. Learn to use the Account Actions to build a secure authentication flow for your application. *** Nearly every SaaS application needs a way for users to sign up, log in, and have their own private account. In Saasio, the authentication system is powered by a special set of **Account Actions** that make this process simple and secure. This guide will walk you through the entire setup, from creating your pages to protecting your content. *** ## Part 1: Create the Authentication Pages First, you need the pages that your users will interact with. Go to the **Pages** tab and create two new, blank pages: * `login`: This will be your main login page. * `signup`: This will be your user registration page. You should also have a `dashboard` page ready. This will be the first page a user sees after they successfully log in. *** ## Part 2: Building the Sign-Up Form & Workflow On your `signup` page, you'll need a simple form. #### **UI Setup:** * A **Form** element. * Three **Input** elements inside the form. Name them `name`, `email`, and `password` respectively in their properties. Set the password input's `type` to "Password". * A **Button** inside the form with the text "Sign Up". Set its `type` to `Submit`. #### **Workflow Setup:** Select your main **Form** element and open the **Logic** tab. We will use the form's **"On Submit"** trigger. from the **Account Actions** category, choose **"Sign up and Login with credentials"**. The action needs to know where to get the user's details. You will map each parameter to the value from your form's state: * **Name:** Bind this to the `Data submitted from form`'s `name` property. * **Email:** Bind this to the `Data submitted from form`'s `email` property. * **Password:** Bind this to the `Data submitted from form`'s `password` property. After the sign-up action is successful, you want to send the user to their new dashboard. * Add another action: **"Go to page..."**. * Set the destination to your `dashboard` page. *** ## Part 3: Building the Login Form & Workflow On your `login` page, the process is very similar. #### **UI Setup:** * A **Form** element. * Two **Input** elements inside the form, named `email` and `password`. * A **Submit Button** with the text "Login". #### **Workflow Setup:** 1. Select the **Form** element and open the **Logic** tab for the **"On Submit"** trigger. 2. Add the **"Login with credentials"** action from the Account Actions category. 3. Bind the `Email` and `Password` parameters to your `Data submitted from form`'s `email` and `password` properties. 4. Add a **"Go to page..."** action to redirect the user to the `dashboard` on a successful login. *** *** ## Part 4: Protecting Your Pages Now that users can log in, you must protect pages like the `dashboard` so that only authenticated users can access them. This is done by creating a special workflow that runs the moment a page begins to load, acting as a security checkpoint. This workflow will check if the user is logged in. If they are not, it will redirect them away before any of the page's content is shown. In the **Pages** tab in the left panel, find and select the page you want to protect (e.g., `dashboard`). With the page selected, click the **Logic** tab in the top navigation bar. This opens the workflow editor for the page's default trigger, which is **"On Page Load"**. This workflow will be empty by default. The first and only action we need in this workflow is the **"Conditioner"**. This will be our security check. * From the **Custom Logic** category, choose the **"Conditioner"** action. Now, we need to set up the rule for our check. * **Condition to Check:** We want to check if the current user is **not** authenticated. * Click into the value field to open the expression editor. 1. Select the **`Current User`** data source. 2. The `Current User` object has a `status` property. Select **`status`**. 3. Add an "is equal to" (`=`) operation. 4. For the second value, choose a static `Text` value of **`unauthenticated`**. Your final condition should read: **`Current User's status = unauthenticated`**. This is the most important part. The actions in the **`Actions if True`** branch will only run if the user is *not* logged in. * Inside the `Actions if True` branch, click **"+ Add Action"**. * Add a **"Go to page..."** action. * Set the **Destination Page** to your `login` page. **What about 'Actions if False'?** You can leave the `Actions if False` branch completely empty. If the condition is false (meaning the user **is** authenticated), the workflow will simply do nothing and allow the page to load normally for the logged-in user. Now, your `dashboard` page is secure. When it loads, the workflow runs instantly. If the user is unauthenticated, they are immediately redirected to the login page without ever seeing the protected content. You can replicate this simple one-action workflow on any page you need to protect. *** ## Part 5: Building the Logout Workflow Finally, you need a way for users to log out. Place a "Logout" button somewhere in your app (like in the navigation bar). This button does **not** need to be in a form. 1. Select the "Logout" button and open its **Logic** tab for the **"On Click"** trigger. 2. Add the **"Logout"** action from the Account Actions category. 3. Add a **"Go to page..."** action to redirect the user to your `login` or homepage after they have been logged out. # Tutorial: User Roles and Permissions Source: https://docs.saasio.io/tutorials/user-roles-and-permissions Learn how to implement a role-based permission system. This guide covers adding an 'admin' role to your users and creating a protected admin-only section. *** As your application grows, you'll need a way to control what different types of users can see and do. For example, you might have regular "Users" and "Admins" who have special privileges. This is known as **Role-Based Access Control (RBAC)**. In this tutorial, we will add a `role` field to our users and create a protected "Admin Panel" that only users with the "admin" role can access. *** ## Prerequisites You must have a fully functional authentication system, as covered in the **"User Authentication"** tutorial. You need: * A `Users` Data Table. * Login, Sign-up, and Logout workflows. * A protected `dashboard` page. *** ## Part 1: Add a "Role" Field to Your Users First, we need a way to store each user's role in the database. To prevent typos and ensure consistency, let's create an Option Set for our roles. * Go to **Data → Option Sets**. * Create a new set named `AppRoles`. * Add two options: `user` and `admin`. * Go to **Data → Data Tables** and select your `Users` table. * Click **"+ Add new field"**. * Create a new field with the following properties: * **Field Name:** `role` * **Data Type:** Select your newly created `AppRoles` Option Set. * **Default Value:** Set the default value to `user`. This ensures that every new user who signs up is automatically assigned the standard "user" role. *** ## Part 2: Manually Assign an Admin Role for Testing For this tutorial, we need an admin user to test with. 1. **Go to the App Data tab:** Navigate to **Data → App Data**. 2. **Select your `Users` table.** 3. **Find your user account** (or any user you want to make an admin). 4. **Click into the `role` field** for that user and manually change its value from `user` to `admin`. *** ## Part 3: Create the Admin-Only Section Now, let's build a page that only admins should be able to see. 1. **Create an `admin` page:** Go to the **Pages** tab and create a new page named `admin`. You can add some text like "Welcome to the Admin Panel". 2. **Protect the page:** We will use the same page protection pattern from the authentication tutorial, but with an extra check. * Select the `admin` page and open its **"On Page Load"** workflow. * Add a **Conditioner** action. * **Condition:** We need to check if the user is *not* an admin. The expression should be: `Current User's user.role` `is not equal to` `"admin"`. * **Actions if True:** If the user is NOT an admin, redirect them away. Add a **"Go to page..."** action and send them to your main `dashboard` or a "Not Authorized" page. * **Actions if False:** Leave this empty. If the user *is* an admin, the workflow will do nothing, and the page will load normally. This workflow acts as a gatekeeper. It runs the moment the page loads and immediately redirects anyone who doesn't have the "admin" role, ensuring they never see the protected content. *** ## Part 4: Conditionally Show UI Elements You can also use the user's role to show or hide specific buttons or links within your application. For example, let's add a link to the Admin Panel that only admins can see. * Go to your main `dashboard` page. * Add a new **Button** or **Link** element with the text "Admin Panel". * Create an **"On Click"** workflow for this new button. - Add a **"Go to page..."** action that navigates to your `admin` page. This is the key step. We only want this button to be visible to admins. * Select the "Admin Panel" button. * Go to the **Conditions** tab in the **Left Panel**. * Add a new condition: * **Condition Logic:** Check if `Current User's user.role` `is not equal to` `"admin"`. * **Style Change:** In the `styles` section, set the `display` property to `none`. Now, the "Admin Panel" button will be completely hidden for any user who does not have the "admin" role, creating a seamless and secure user experience. You can apply this same conditional logic to any element in your application. # Exploring the Visual Editor Source: https://docs.saasio.io/visual-editor A detailed tour of the Saasio Visual Editor. Learn about the canvas, panels, and the primary tools you'll use to build your SaaS application. *** The Saasio Visual Editor is the heart of the platform. It's an all-in-one workspace where you will design your user interface, manage your app's data, build workflows, and configure settings. Understanding its layout is the key to building quickly and efficiently. A complete overview of the Saasio Visual Editor. The editor is divided into four main areas: *** ## Top Navigation Bar: Quick Actions The top bar is your primary toolkit for building your application. It provides direct access to every major function you'll need, from designing the UI to deploying your project. Generate entire page layouts and components instantly from a simple text prompt. Access pre-built, professional components from [Shadcn](https://ui.shadcn.com/), [Magic UI](https://magicui.design/), and [Aceternity UI](https://ui.aceternity.com/) , etc. Add fundamental building blocks like text, containers, images, and forms to your page. Manage your database tables, create form schemas, and connect to any external API, etc. Define the step-by-step workflows and actions that power your application's interactivity. Configure app-level settings such as your custom domain, SEO, and integrations. Instantly check your design's responsiveness across desktop, tablet, and mobile screens. Preview, publish, and manage your application's live deployments with a single click. *** ## Left Panel: Project Structure The left panel is your command center for managing the assets, logic, and overall structure of your application. It contains several powerful tabs for different functions. Create new pages, organize them into folders, and navigate between the different screens of your application. Define backend API endpoints to handle server-side logic, interact with your database, or connect to external services. View the complete hierarchy of all UI elements on the current page. Select, reorder, and manage elements from this view. Create your own reusable UI components. Build a component once and use it anywhere across your application to maintain consistency. Manage your app's data. Create **State** variables for storing temporary data and upload **Assets** like images, videos, and files. Define powerful rules to dynamically change how elements look and behave based on application state or user interaction. Let the AI generate a complete design system for your app, including color palettes and typography for both light and dark themes. Design and manage professional, reusable email templates for transactional emails, notifications, and marketing campaigns. *** ## Central Canvas: Your Workspace The canvas is the large, interactive area in the center of the editor. This is your main building space. * **Visual Building:** Drag and drop elements from the UI Libraries and Elements tabs directly onto the canvas. * **Direct Manipulation:** Click, resize, and rearrange elements to create your desired layout. * **Live Preview:** The canvas provides a real-time preview of what your page will look like. *** ## Right Panel: Properties & Styling The right panel is your contextual toolkit for fine-tuning every element. When you select a component on the canvas, this panel dynamically updates to show all of its available settings. This is where you control how your elements look, feel, and behave across different screen sizes. Adjust visual styles like colors, backgrounds, fonts, borders, and shadows to match your brand. Control the element's size, spacing (padding and margins), and exact positioning on the page. Configure element-specific properties, such as the URL for a link or the validation rules for a form input. Bring your UI to life with production-grade animations powered by [motion.dev](https://motion.dev) (formerly Framer Motion). Customize how elements appear and respond to user interaction.