On Bubble, one of the most common use cases we see our users building is a one- or even two-sided marketplace. We say “two-sided” because Bubble’s extensive no-code programming capabilities allow you to not only publish and sell products yourself, but to host other sellers who can build and manage their own shops through your platform — all without needing to know how to code. 

Whether you’re looking to jumpstart your own marketplace app or learn more about Bubble’s advanced features for a different app idea, you can level up your Bubble skills by following our own team members in this two-part how-to-build guide.

Part 1: Step-by-step instructions on building a marketplace without code (Basics)

Here, Bubble VP of Product Allen Yang walks you through the basics of setting up a product search page for a bookseller’s marketplace app. He’ll go through setting up the database from scratch before building the foundational design of the page, which includes a list of products that can be searched and filtered.

When you’re ready to get started, you can watch Allen’s video below — and don’t forget to stick around for the Q&A after! To help you follow along in your own app, we’ve also summarized his step-by-step instructions below.

Subscribe to the Bubble YouTube channel to get notified about tutorials, explainers, and deep-dives

1. Set up your database structure (1:20)

Go to the data tab. Under the data types subtab, create two custom data types: “Shop” and “Product.” Then add the following custom fields:


  • Name (text)


  • Name (text)
  • Description (text)
  • Price (number)
  • SellerShop (Shop)

Next, it’s time to define your product data type further ‌using static options. From the option sets subtab, create three new option sets:

Book Genre:

  • Fantasy
  • Romance
  • Thriller

Book Format:

  • Print
  • Digital
  • Audiobook

Book Type:

  • Novel
  • Comic Book
  • Short Stories

Feel free to add more options if you want! Just remember, this will impact the filters and metadata you display later.

Now, go back to your product data type and add your option sets to its structure. Hit Create a new field and select each option set you just created from the Field type dropdown. To keep it simple, match the Field name to the Field type

Congrats — you now have a comprehensive database structure! 

2. Add sample data (4:56)

Sample data is super useful for testing and demoing, and it becomes even more critical when you’ve added more complex features.

To create sample data, go to the app data subtab under the data tab and hit New entry. Now you can manually add as many sample shops and products as you want.

Note: Allen skips ahead at this part of the video, but he adds three shops with two products each. Pause here and add as much sample data as you’d like. Pro tip: Try to make your products as varied as possible to make it easier to test the search and filter functionality later. 

3. Build the marketplace search page (5:35)

Create a new page called “search” from the page dropdown. Double-click on the page to open the property editor and set the Container layout to Column. 

Time to set up your page skeleton. Open the component library from the toolbar at the top of your editor and drag in a header and footer. Then add a blank row group to the left-hand side for your main content. Change its layout to Row, and uncheck Make this element fixed-width

Tip: As you work, it’s useful to rename your elements to keep them organized (we'll mention Allen’s naming structure as we go). 

Now we can start building the main search page content: Rename the row group “G Dashboard” (Group Dashboard). Then add a new group and name it “G Filters.” You can add a Background style from the dropdown so it stands out on the page. 

Next to your G Filters group, add a repeating group and rename it “RG Products” (Repeating Group Products). Give it the following settings:

  • Layout: Uncheck Make this element fixed-width so that it expands to its parent container and set Cell’s container layout to Column to give it a top-to-bottom layout.
  • Appearance: Set Type of content to “Product” and Data source to “Do a search for Products.”

Now add a text element. Under Layout, uncheck the box for fixed-width and make the Min height 5px. From Appearance, hit Insert dynamic data to create the dynamic expression “Current cell’s Product’s Name.” 

You’ll be adding several more text elements to this repeating group. You can copy and paste the first text element you added to keep the settings the same. Add one dynamic expression to each: 

  • “Current cell’s Product’s Description”
  • “Current cell’s Product’s Price: formatted as Currency”
  • “Current cell’s Product’s SellerShop’s Name”
  • “Current cell’s Product’s Genre’s display”
  • “Current cell’s Product’s Format’s display”
  • “Current cell’s Product’s Type’s display”

Take however much time you need to clean up this design — play with the font, spacing, and alignment to customize your app. 

Lastly, select all the content in your repeating group cell, right-click, and group elements into a Row container. Then drop a new group to the right. Drag an Icon into this group and select a heart. Then add a Button and label it Add to cart. Adjust the spacing and alignment to match your preference; Allen centers the heart and button and applies gap spacing.

Customizing your design involves many small steps. Your design doesn’t have to match the tutorial exactly. Preview your work and adjust as you need. For instance, Allen previews the page and sees that the repeating group only shows four products. To fix this, uncheck the Set fixed number of rows option on the repeating group. You’re all set!

4. Set up filters (14:20)

Now add a way for users to filter their search by adding the following elements to the G Filters group:

  • 1 input (to search)
  • 1 slider (to select maximum price)
  • 4 radio buttons (to select between different options)
  • 1 button (to clear filters)

You can also add text elements to label your filters. Allen adds the first text element and adjusts its min and max width and height settings, then duplicates it for each label.

Now, let’s tackle each input one by one: 

Search bar input: Nothing to program here — users only need to type into it.

Slider input: Adjust the label to include dynamic data. After “Price filter,” insert the dynamic expression “MaxSliderInputA’s value:formatted as $.” Then set the Min and Max value to 0 and 100 and the Step to 5 to make the slider less granular. Lastly, set the Default value to your max price so your app shows all products and all prices by default.

Radio buttons: For the Genre radio button, select Dynamic Choices under Choices style. Select Book Genre for Type of choices. Make the Choices source “All Book Genre,” and the Option caption “Current option’s Display.” Do the same for the Book Format and Book Type radio buttons

The Shop radio button will be slightly different since it will display dynamic data (sellers) instead of option sets. Select Shop for Type of choices. Make the Choices source “Do a search for Shops,” and the Option caption “Current option’s Name.” 

Button: For now, just label it “Clear Filters” and center it or adjust its width and height as needed. You’ll be revisiting it later.

Your filters are ready to go! Now it’s time to connect it all together.

5. Connect filters to repeating group (19:18)

Connecting your repeating group and filters together is a super simple and quick process.

All you have to do is modify the Data source for your RG Products repeating group. Earlier, you made it a straightforward database search. But if you click the dynamic expression, you can add constraints to this search that reference each filter.

Let’s start with Genre. Add a new constraint to the database search and set Genre = “RadioButtons A’s value.”

If you preview your app to test it, you may not see anything show up in the repeating group anymore. Don’t panic! This is expected, and it has a simple fix: Go back to the Data source expression and check the Ignore empty constraints box. Now, if any of your inputs are empty, the database search won’t apply that filter.

Now you can apply the same logic to the other radio buttons — just add a constraint that sets the field to be equal to that radio button element’s value.

Add "Price ≤ SliderInput A's value" as a constraint. This ensures that only products with a price equal to or below the slider input's value are shown.

Lastly, add another constraint for your search input: Name contains keyword(s) Input A’s value.” Allen uses “contains keyword” instead of just “contains so that the searches aren't case-sensitive.

Now, your search and filtering functionality is all set up! 

6. Implement the “Clear Filters” button functionality (23:00)

There’s just one more thing to do: Users need a quick way to reset all of their selected filters if they want to start again from scratch.

Select the button you added to the bottom of the G Filters group and click Add workflow.

Now, in the Workflow tab, add one action to the new workflow. Under Element actions, select Reset data. Select the entire G Filters group.

Now, if you preview your app, you should have a fully functioning search page! Customers can browse products, search for specific titles, and filter results.

7. Optional design enhancements (23:45)

You've completed all of the functionality needed before jumping into part two. But before he signs off, Allen showcases a glow-up of his basic design.

We won’t cover any step-by-step instructions on how to achieve this updated design, but here are a few customizations to consider:

  • Add buffer and white space between the design and page edges
  • Add an image next to each product listing
  • Customize the font and styles in the repeating group
  • Add more elements to the header to set the stage for potential development paths 
Explore the design yourself by visiting the read-only editor of Allen’s app, including the basic and updated designs.

Part 2: Step-by-step instructions on enhancing your marketplace with backend workflows (Advanced)

The second marketplace how-to is taught by John Carter and Maria Posa, two of the talented Bubble Developers who build and maintain bubble.io. They’ll pick up exactly where Allen left off and evolve the bookseller marketplace. 

Allen’s tutorial focused on building features that customers need to browse. Now, John and Maria will tackle the aspects a seller would need in order to engage with their customers. 

They cover follow functionality — allowing customers to “like” their favorite sellers. Then they show you how to enable sellers to draft, delete, and schedule emails to their customer base, and take advantage of one of Bubble’s most powerful areas of development: backend workflows. 

Here’s their video, along with Q&A after. Keep reading for step-by-step instructions.

Subscribe to the Bubble YouTube channel to get notified about tutorials, explainers, and deep-dives.

1. Update the database structure (2:15)

To prepare for the additional features you’ll be adding, let’s start by expanding your data structure in the data tab.

First, create a new data type called “Follow.” This will let users follow shops they like, and it records an interaction between two parties in your database. To do this, the ”Follow” data type needs two custom fields. Name the first “Shop” and set the Field type to Shop. Name the second “User” and set the Field type to User.

You’ll also want to attach a record of this action to the user’s entry in the database, so in the “User “data type, add a new custom field: “Shops I’m Following” which will be type Shop. Make sure to check This field is a list (multiple entries).

Now you can track when a customer follows or unfollows a shop. Later, you’ll build a functionality that lets shops email their followers. To set this up, create a new custom data type called “Email” and add the following custom fields:

  • “Body” (text)
  • “Subject Line” (text)
  • “Shop” (Shop)
  • “User” (User)
  • “Send Date” (date)

You’re ready to tackle the next part!

2. Build the “follow” functionality (3:38)

Head back to the design tab. This should be the page you built previously — your search page. It’s time to program the heart icon you added earlier so that users can follow shops. 

First, add a popup named “Popup Follow Shop” to confirm the user wants to follow this shop. It should have some basic text explaining this to the user along with a button to confirm the follow. Feel free to design the popup however you wish, or reference how John designed his in the video.

Next, add a workflow to the heart icon. Select Show an element and select your popup from the dropdown. If you have any text in your popup that references the particular shop, you can also add a second step to your workflow. Display data in Popup Follow Shop. 

Now that customers can open the popup, add a workflow to the popup’s follow button. The first step should be to Create a new Follow. Define your two fields: User should equal the Current User. Shop should equal the shop selling the product of that cell. 

You’ll also want to update the list of shops that the user is following. To do this, add a second step to the workflow: Make changes to Current User. Change the field Shops I’m Following to include add Result of Step 1. Now you users can follow shops!

But what about unfollowing? Thankfully, you can repurpose everything you just built for unfollowing as well.

On the workflow you just created, add a conditional at the bottom of the property editor next to Only when. You want this event to fire Only when Current User’s Shops I’m Following: each item’s Shop doesn’t contain and then insert the shop in question. In other words, this means a user can only follow the shop if it isn’t already in their follow list. 

To create the inverse of this workflow event, quickly copy and paste it. Then change the conditional expression from doesn’t contain to contains. This way, it'll only fire if the user is already following the shop. Delete the copied actions and reconstruct the workflow as needed. 

First, find the Delete a thing action and set it to Delete a Follow. You’ll need to perform a search for follows and constrain the search so that the user of that follow is Current User and the shop of that follow is the current shop. These two constraints should pinpoint the right follow from your database. But you must confirm you only want to pull one item from the search by adding :first item to the search. 

Now you need to make sure the UI (user interface) changes based on whether a user is following the shop. The button on your popup will say Follow no matter what. But you can copy the conditional you added to the workflow event and use it again. Move to the design tab and find the button on that popup. Paste in that conditional in the When field. Then change the text to say “Unfollow.”

You can use the same conditional logic for the word "Follow" or the heart icon in your popup. 

This completes the basic functionality of your app needed on the customer’s side. Now it’s time to tackle the seller’s experience.

3. Set up the shop admin page (9:12)

Create a new page called “shop-admin.” Feel free to pull in any header or footers if you want. Otherwise, you’ll be building the main content of this page from scratch.

The tutorial fast-forwards through John’s overall design process, as it’s fairly similar to how Allen designed the initial search page. You can customize this page further if you want, but for this tutorial you’ll need the following elements:

  • A button with the text “Send Email”
  • A repeating group that displays a list of created emails and includes fields for subject line, body, send date, and a “delete” button

To pull the appropriate data source on the email repeating group, you’ll need to set the type of content to Email and the Data source to “Search for emails” in which Shop = Current User’s Shop.

4. Create and delete email functionality (11:32)

You’re ready to look at‌ creating, scheduling, and deleting these emails! You’ll start by building two popups to set up the logic that creates and deletes email records. 

First will be a schedule email popup for creating an email. This will need inputs to capture the subject line, the body, and the send date. The second will be a much simpler delete popup, which just acts as a confirmation if someone presses the delete button or icon in the cell.

Once you have these both designed the way you want, let’s hook up the logic for creation and deletion. 

To create an email, make sure the button you added to your page opens the schedule email popup. Then add a workflow to the button: Create a new Email. Define the subject line, the body, and the send date using the inputs from the popup. Define the shop attached to the Email as Current User’s Shop (the shop the current user manages). And define the User as Current User (the sender). 

Tip: It’s a good practice to also add a step that resets all relevant inputs so that the fields are blank the next time the user presses the button. 

Now it’s time to program your delete functionality. On your new delete popup, specify the Type of content to be Email. Leave the data source empty. Now add a workflow to your delete button. Add a step to “Display data in a group/popup” and select your delete popup. This allows the page to send data right before it displays the popup. (in this case, the email). Then, like before, add a step to show the popup. Now add a workflow to the cancel email button. Add a step that Deletes the Parent Group’s Email, then hide the popup.

Now you have a means of creating and deleting emails. But none of them are actually being scheduled to send. For that, it’s time to turn to backend workflows.

5. Backend workflows: scheduling and ending an email (14:12)

Backend workflows are only available on apps with a paid plan, so you’ll need to upgrade your app or try a free trial.

To enable backend workflows in your app, you’ll need to go to the settings tab and navigate to the API subtab. Click Enable Workflow API and backend workflows. At the very bottom of the page dropdown, you should now see an option to view backend workflows.

The backend workflows tab has different options compared to the regular workflow tab. Backend workflows run independently of any page in your app, so you’ll also see there’s no specific page you’re on.

Let’s review what you’ll be programming: You’ll create a workflow that sends emails to a list of users and another workflow that schedules the first workflow to run at a later time.

To start, create a new API workflow. Call it “schedule-email.” Then add a parameter — this tells the workflow that it'll need to interact with a data record, which you can dynamically assign later. For now, create a parameter called “email” and change its Type to Email.

Next, create another API workflow and call it “send-email.” Same thing here — add a parameter called “email” which is Type Email. Again, the actual email will be provided later; this just tells your backend workflows what kind of data to expect.

Now you’ve set up your two backend workflows. Let’s program them. 

For “schedule-email,” add an action and select Schedule API workflow. Set API Workflow to “send-email” and for Scheduled date set the parameter to email’s Send Date.

Important: Once an email has been scheduled, a custom ID is generated for that workflow. Earlier, you created functionality to delete emails from your database, but this doesn’t delete the record of a scheduled workflow. It’s a good idea to capture that ID and attach it to the email. 

As a short detour, head to the data tab and add a new Custom field to the Email data type called “Workflow ID.” Bubble generates this ID as a string of characters, so keep the Type as text.

Now, back in backend workflows, under “schedule-email,” add a second action to Make changes to an Email. The email to change is just the email record parameter. Set the Workflow ID field to equal Result of Step 1. You’re attaching the record of the scheduling to the email itself.

OK, now let’s tackle the “send-email” workflow. Find the send email action and have the email sent to Current User’s email. And then bcc: Do a search for Follows with a constraint so that the follows are related to the right shop. From that list of Follows, pull :each item’s User’s email. For Subject and Body, pull the parameter email’s subject line and body.

Terrific! Now your “schedule-email” workflow will fire the “send-email” workflow at the appropriate time. Now, you just need to make sure that you fire “schedule-email.”

Go back to the shop-admin page. Find the workflow that triggers when a new email is created. The first action in this workflow should already be to Create a new Email — you did that earlier. After that step, add Schedule API workflow and have that workflow be “schedule-email.” 

This step asks for a Scheduled date. However, this isn't asking when you want to send the email. Instead, it's asking when you want to trigger the "schedule-email" workflow — right now. Enter Current date/time. And for the email, select Result of Step 1. 

6. Backend workflows: canceling an email (17:44)

One more backend workflow to create: canceling a scheduled email. Back in the backend workflows tab, create a new API workflow called “cancel-email.”

Like before, it’ll just take in an email parameter.

Your first action will be to Cancel a scheduled API workflow. You’ll need to reference a particular ID to complete this step. Thankfully, you saved that workflow ID onto the email itself. So all you need for the Scheduled API ID is the email’s Workflow ID.

Finally, you just need to trigger this “cancel-email” workflow from the frontend of your app. So, go back to the shop-admin page and find the delete popup. Make sure to add a step to the cancel email button to Schedule API workflow: cancel-email. Make sure it happens right now by setting Scheduled date as Current date/time. And pass through the email on the popup as the email to pass through for the parameter. 

Note: You have the step to delete the email entry in this workflow from earlier. Keep that step and have it happen after the Schedule API workflow action. Or, remove it from this workflow and add it to the backend workflow instead. It’s up to you!

7. Final testing and logs tab (18:50)

All your functionality is set! To test this, you need some sample users to the database. You'll want at least two users: one as the Admin (who'll send the email) and one as a Follower (who'll receive the email).

Once you create and schedule an email to be sent:

  • In the data tab > app data: Verify that the email record has been created and stored in the database.
  • In the logs tab > scheduler: Verify that the email has been scheduled and is waiting to be sent by hitting the Show button to see all scheduled API workflows.

Finally, if you test canceling the email, you should see that the email disappears from your database and the Scheduler of the Logs tab.

And just like that, you have a working frontend and backend for a two-sided marketplace MVP. Congratulations!

Explore John and Maria’s setup yourself by visiting the read-only editor of their app.

Recap: Q&A with Allen, John, and Maria

Here's a summary of the Q&A that followed each live session with Allen, John, and Maria from our virtual How to Build Day: 

Now that we have a page for browsing the marketplace, what would you want to add next to the frontend?

Allen: “I’d polish the frontend a bit. We can add more branding, colors, and styles. Bubble has features that help us standardize these things across our app. And a well-designed page uses the same color palette, element sizes, and shapes throughout the website. It could be good to add sorting options, like by genre or book type. And when you click on a genre or book type, it automatically filters the repeating group to show only those items. We could also add the ability to mark that you've read or own a certain book, so it's not shown as an option to buy.”

What would you say is the underlying difference between using option sets versus data types?

Allen: “It's a common question because data types and option sets on Bubble seem similar. Option sets are useful when you have a fixed list of options for a thing. For example, days of the week or genres in a book marketplace app. However, if you want more flexibility or user interaction, like creating or editing options, you should use a database and data type.

For users who have sample data in a CSV file, is there a way that they can upload that into Bubble?

Allen: “Sure, in the editor, go to the data tab and the app data sub tab. You can upload a CSV file and map its columns to the fields of your data type. There's also a button on the app data sub tab that lets you modify records in bulk based on a CSV. You can also export all records to a CSV.”

Could you speak on what it means to make sure all elements are responsive?

Allen: “Responsive design means thinking about how the layout adapts to different screen sizes. I prefer to first design for the default width (usually laptop size) and then fine-tune the design for different screen widths. Each element has a default behavior as the screen width changes. Sometimes, it tries to adapt, and sometimes it doesn't. When designing responsively, first check the default appearance at different screen widths. Then, adjust individual elements as needed. 

Here's an extra tip: sometimes it's easier to create a separate element that only appears on certain screen lists. That's better than trying to use the same container and force it to work across all screen lists.”

Would it be possible for the interface to just display the first ten rows and as a user is scrolling down, display the next ten and so on and so forth?

Allen: “Bubble tries to optimize loading data onto a page by default. But if you want a more specific experience around lazy loading, like the one described in the question, it's entirely possible. You just need to combine a few different building blocks that Bubble offers. Let’s say when the page loads, we display ten items. To load more, we have the user click a button. When the button is clicked, a workflow can update the query of the repeating group or table to show more items. That’s just one method, there’s plenty of others.”

How do you know what data types you need, and then what fields to add and then how to get them to interact with each other?

Allen: “It’s up to you, but consider data types like real-life objects or agents in a system. Like books or sellers in a marketplace. Each object or agent has different properties, like a book's title or a seller's name. These properties become fields in your database.”

Can you speak to Bubble being a relational database? And how do you decide when to use a one-to-one database relationship versus one-to-many?

Allen: “If you're a software engineer, you might be familiar with the idea of a ‘join.’ If you have information in one type of data that can be displayed as a record from another type of data, think about how you want to connect them. For example, let's say you have a data type for books and another for sellers. You could link them by making the ‘seller’ field in the book data type part of the seller data type. This means that each book would be connected to a specific seller. Another way to do it would be to keep a list of books in the seller data type. This means each seller would be connected to a list of books they are selling.”

Maria: “I’d recommend not to keep long lists in one field. For example, in a marketplace app, you could store the list of users following a shop or the shops a user is following. The first list can grow too big quickly if thousands of users follow the same shop. The second list is safer because it's unlikely for one person to follow thousands of shops. We used one of these list fields and a separate table to relate a user to a shop. The best choice depends on your app and what you need to show in your design.”

What's the difference between a frontend and backend workflow?

Maria: “Backend workflows run on the server, while frontend workflows run on the user's computer. Backend workflows are asynchronous, meaning you can't guarantee they'll be completed by a certain time. Frontend workflows can be synchronous, so you can predict their order. They require a user to be on the page, so if the page closes, the workflow will stop. Backend workflows will continue executing even if the user leaves the page.”

Could you explain what an endpoint is?

John: “We use endpoints and backend workflow / API workflow fairly interchangeably. All this is describing is one of those API workflows that you want to set up in your application and call from a variety of places. So like the API workflow that Maria set up at the end of her video to send out these emails, that is an endpoint.

How do you decide when to add in delays as it is going through these iterative loops in a backend workflow?

Maria: “If you plan to run a backend workflow on an extensive list of items, it's a good idea to spread them out. This way, you avoid overwhelming your app and potentially showing users problems on the frontend. If you're trying to process a very large list over an extended period, consider using recursive workflows. In short, this is a safety measure you can use when trying to change a lot of data at once.”

Could you provide examples of good use cases for when to use backend workflows?

John: “When users create data on your app, you may not want to keep it indefinitely. You can set up a background process to schedule the removal of records after a certain period, like every year. This way, you don't have to hold onto the data for too long. Email campaigns are another example. Once a user signs up for your app, you can schedule a series of emails to welcome them, remind them of features, and more.”

We talked about sending emails. Does Bubble support the sending of SMS messages? Can you integrate with a third-party service like Twilio?

Maria: “There should be plugins on the marketplace to help with this. Otherwise, you could use the API Connector to hook up to Twilio.”

What’s your advice on debugging?

John: “For frontend debugging, we have the debugger tool, which you can use while running workflows to test and verify expected results. For backend debugging, we use server logs in the settings tab to check for any conditions that are preventing actions from running. Another debugging tip is to manually insert an action in workflows to log a record or send an email when something's not working correctly. This helps you understand what's happening at each step.”

How should users go about improving search performance, especially when there's a really large data source to search through?

Maria: “Bubble automatically creates indexes when needed. To improve scalability, consider your database design. Sometimes, adding a field to a data type allows direct searching, instead of searching linked data types in a repeating group. Best practices for scalability are learned over time.”

How can users iterate and add features to a live app? So once you've deployed an app, what does it look like to then add features on top of that and then to somehow merge changes into the live system?

John: “Version control is a tool built into Bubble. It helps you manage changes to your app. When you add new features, you can use version control to make a copy of your live app, build the new features on the copy, test the new features to make sure they work, and merge the changes back to your live app.”

Keep up the good work! Check out the next recording from How to Build Day, How to Build an AI-Generated Recipe App With No Code.