# How to display dynamic ads in Bubble.io: Step-by-Step Guide

- Tool: Bubble
- Difficulty: Beginner
- Time required: 20-25 min
- Compatibility: All Bubble plans
- Last updated: March 2026

## TL;DR

Build a dynamic ad display system in Bubble by creating an Ad Data Type that stores creatives, targeting rules, and schedule dates. Use conditional searches to rotate ads based on page context or user demographics, track impressions and clicks with workflow counters, and build an advertiser dashboard for managing campaigns — all without writing code.

## Displaying Dynamic Ads in Your Bubble App

Whether you are building a content platform, a marketplace, or a community app, displaying ads is a common monetization strategy. This tutorial shows you how to build your own ad system in Bubble — storing ad creatives in the database, rotating them based on rules, tracking performance metrics, and giving advertisers a management interface. This is an alternative to embedding third-party ad networks like Google AdSense.

## Before you start

- A Bubble account with an app open in the editor
- User authentication set up for advertisers and admins
- Basic understanding of Data Types, searches, and workflows

## Step-by-step guide

### 1. Create the Ad and Campaign Data Types

Go to Data tab → Data types. Create 'AdCampaign' with fields: name (text), advertiser (User), start_date (date), end_date (date), budget (number), status (text: active/paused/ended), target_category (text). Create 'Ad' with fields: campaign (AdCampaign), image (image), headline (text), destination_url (text), placement (text: banner/sidebar/inline), impressions (number, default 0), clicks (number, default 0), is_active (yes/no).

**Expected result:** Two Data Types (AdCampaign and Ad) are created with fields for targeting, tracking, and management.

### 2. Build the ad display element on content pages

On the page where you want ads to appear, add a Group element sized for your ad placement (e.g., 728x90 for a banner). Set the Group's Type of content to 'Ad' and its Data source to: 'Do a search for Ads' with constraints: is_active = yes, placement = 'banner', and campaign's status = 'active', campaign's start_date <= Current date/time, campaign's end_date >= Current date/time. Add ':random item' at the end to select a random ad from eligible ones. Inside the group, add an Image element showing 'Parent group's Ad's image' and a Text element for the headline.

> Pro tip: Use ':random item' for simple rotation. For weighted rotation based on budget, add a 'priority' number field and sort by it descending, then take the first item.

**Expected result:** A random active ad appears in the banner slot, respecting date ranges and status filters.

### 3. Track impressions when ads are displayed

Add a 'Page is loaded' workflow (or a 'Do when condition is true' event that fires when the ad group's data source is not empty). In this workflow, add the action: 'Make changes to a thing' → Thing to change: the ad group's data source (the Ad displayed) → impressions = impressions + 1. This increments the impression counter each time the ad is shown to a user. To avoid counting the same user twice per session, add a custom state 'impression_tracked' (yes/no) and check it in the Only when condition.

**Expected result:** Each time an ad is displayed to a user, its impression count increases by one.

### 4. Track clicks when users interact with ads

Make the ad group clickable by adding a workflow: 'When Group Ad is clicked.' In this workflow, add two actions in order: (1) Make changes to Parent group's Ad → clicks = clicks + 1, (2) Open an external website → URL = Parent group's Ad's destination_url. This records the click and then sends the user to the advertiser's landing page. The click-through rate (CTR) can be calculated as clicks / impressions in your reporting.

**Expected result:** Clicking an ad increments its click counter and redirects the user to the advertiser's destination URL.

### 5. Build a basic advertiser dashboard

Create a page called 'advertiser-dashboard.' Add a Repeating Group with Type 'AdCampaign' and Data source: 'Do a search for AdCampaigns where advertiser = Current User.' Each cell shows: campaign name, status, start/end dates, and aggregate metrics. For metrics, add Text elements showing: 'Do a search for Ads where campaign = Current cell's AdCampaign:each item's impressions:sum' for total impressions and similar for clicks. Add buttons for 'Pause Campaign' (Make changes → status = 'paused') and 'Edit Campaign.'

**Expected result:** Advertisers see all their campaigns with performance metrics and can pause or manage them.

### 6. Add category-based ad targeting

To show relevant ads based on page content, pass a category context to your ad search. On a blog post page, for example, the post has a category field. Update your ad search constraint to include: target_category = Current Page Post's category (or target_category is empty for untargeted ads). Use 'Ignore empty constraints' if you want ads with no target_category to show everywhere. This ensures software ads appear on tech articles and food ads appear on recipe pages.

**Expected result:** Ads are filtered by category relevance, showing targeted ads on matching content pages and general ads elsewhere.

## Complete code example

File: `Workflow summary`

```text
DYNAMIC AD SYSTEM — DATA MODEL & WORKFLOWS
=============================================

Data Types:
  AdCampaign: name, advertiser (User), start_date, end_date,
              budget, status (active/paused/ended), target_category
  Ad: campaign (AdCampaign), image, headline, destination_url,
      placement (banner/sidebar/inline), impressions (number),
      clicks (number), is_active (yes/no)

Ad Display Logic:
  Group Ad Banner (728x90)
    Type: Ad
    Data source: Search Ads where
      is_active = yes
      placement = 'banner'
      campaign's status = 'active'
      campaign's start_date <= now
      campaign's end_date >= now
      campaign's target_category = page context category
    :random item

Impression Tracking:
  Trigger: Page is loaded (or Do when ad group data is not empty)
  Only when: page's impression_tracked is no
  Action 1: Make changes to Group Ad's Ad → impressions + 1
  Action 2: Set state impression_tracked = yes

Click Tracking:
  Trigger: When Group Ad is clicked
  Action 1: Make changes to Group Ad's Ad → clicks + 1
  Action 2: Open external website → Ad's destination_url

Advertiser Dashboard:
  Page: advertiser-dashboard
  RG: AdCampaigns where advertiser = Current User
  Metrics: sum of impressions, sum of clicks per campaign
  Actions: Pause/Resume campaign, Edit ads, View stats

Admin Review:
  Page: admin-ads
  RG: All AdCampaigns sorted by creation date
  Actions: Approve/Reject campaigns, Flag policy violations
```

## Common mistakes

- **Not filtering ads by date range** — Without start_date and end_date constraints, expired campaigns continue showing ads after they should have ended. Fix: Always include date constraints in your ad search: start_date <= Current date/time AND end_date >= Current date/time.
- **Counting impressions on every page reload for the same user** — A single user refreshing the page inflates impression counts and gives advertisers inaccurate metrics. Fix: Use a custom state or session cookie check to track whether the impression has already been counted in the current session.
- **Opening the destination URL without recording the click first** — If you navigate away before the click is recorded, the database write may not complete, losing click data. Fix: Place the 'Make changes to Ad' action before the 'Open external website' action in the workflow sequence.

## Best practices

- Always filter ads by active status, date range, and placement to show only relevant, current ads
- Track both impressions and clicks to calculate click-through rates for advertisers
- Use ':random item' for simple ad rotation or a priority field for weighted display
- Implement session-based impression deduplication to avoid inflating metrics
- Give advertisers a self-service dashboard to manage campaigns and view performance
- Add admin approval workflows so ads are reviewed before going live
- Use category targeting to show relevant ads based on page content context

## Frequently asked questions

### Should I build my own ad system or use Google AdSense?

Build your own if you want to sell ad space directly to advertisers and keep 100% of revenue. Use AdSense if you want automated ad filling without managing advertiser relationships. Many apps use both.

### How do I prevent the same ad from showing repeatedly?

Store recently shown ad IDs in a custom state list. Add a constraint to your ad search: unique id is not in page's shown_ads list. After displaying an ad, add its ID to the list.

### Can I charge advertisers based on impressions or clicks?

Yes. With impression and click tracking built in, you can calculate costs as cost-per-thousand-impressions (CPM) or cost-per-click (CPC). Deduct from the campaign budget accordingly using a backend workflow.

### How do I handle ad-blockers?

Since your ads are served from Bubble's own database (not an external ad network), most ad-blockers will not detect them. Your custom ad system is inherently ad-blocker resistant.

### Can I display video ads?

Yes. Add a 'video_url' field to the Ad Data Type and use a Video element or HTML5 video tag in the ad group. Set it to autoplay (muted) for a richer ad experience.

---

Source: https://www.rapidevelopers.com/bubble-tutorial/display-dynamic-ads-in-bubble
© RapidDev — https://www.rapidevelopers.com/bubble-tutorial/display-dynamic-ads-in-bubble
