<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Bubble.io No Code Guides]]></title><description><![CDATA[Know how-tos of No-Code Bubble.io Development processes for optimized performant scalable MVP creation.]]></description><link>https://anishgandhi.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1769407512378/77cf291e-bc67-417a-9aba-6e071a753ff5.png</url><title>Bubble.io No Code Guides</title><link>https://anishgandhi.com</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 21 May 2026 16:30:57 GMT</lastBuildDate><atom:link href="https://anishgandhi.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I Used Claude Code to Build a Full-Stack React App: A Step-by-Step Development Guide]]></title><description><![CDATA[Software development gets mystified. Some call it magic. Others call it chaos.
The truth? It's a disciplined, iterative process where each step builds on the last. I'm going to walk you through exactly how I built React AI UI Generator using Claude C...]]></description><link>https://anishgandhi.com/how-i-used-claude-code-to-build-a-full-stack-react-app-a-step-by-step-development-guide</link><guid isPermaLink="true">https://anishgandhi.com/how-i-used-claude-code-to-build-a-full-stack-react-app-a-step-by-step-development-guide</guid><category><![CDATA[claude-code]]></category><category><![CDATA[React]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[AI development]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[full stack]]></category><category><![CDATA[Tailwind CSS]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Mon, 26 Jan 2026 05:34:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769405398563/9ec9e756-adfd-4593-8661-ade780ddf462.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Software development gets mystified. Some call it magic. Others call it chaos.</p>
<p>The truth? It's a disciplined, iterative process where each step builds on the last. I'm going to walk you through exactly how I built <a target="_blank" href="https://github.com/nocodeanish/react-ui-generator"><strong>React AI UI Generator</strong></a> using <strong>Claude Code</strong> as my AI pair programmer—revealing every decision, every security consideration, and every lesson learned.</p>
<p>Whether you're a developer exploring AI-assisted development or someone evaluating how Claude Code can accelerate your workflow, this step-by-step guide will show you what's actually possible.</p>
<blockquote>
<p>🔗 <strong>Want to explore the code?</strong> The entire project is open source: <a target="_blank" href="https://github.com/nocodeanish/react-ui-generator"><strong>React AI UI Generator on GitHub</strong></a>. Fork it, build on it, or use it as a reference for your own Claude Code projects.</p>
</blockquote>
<hr />
<h2 id="heading-step-1-ship-something-real-before-perfecting-anything">Step 1: Ship Something Real (Before Perfecting Anything)</h2>
<p>Here's a controversial take: <strong>my first commit wasn't a planning document or architecture diagram.</strong></p>
<p>It was a working application - built collaboratively with Claude Code.</p>
<p>Using Claude Code as my AI pair programmer, I described what I wanted to build, and together we created:</p>
<ul>
<li><p><strong>A virtual file system</strong> (components stored in memory, not on disk - enabling instant operations)</p>
</li>
<li><p><strong>Real-time preview</strong> with live code transformation (changes appear instantly as you type)</p>
</li>
<li><p><strong>AI-powered code generation</strong> using the Vercel AI SDK (the app writes React components for you)</p>
</li>
<li><p><strong>User authentication</strong> and project persistence (saving your work between sessions)</p>
</li>
<li><p><strong>Prisma ORM with SQLite</strong> database (structured storage that scales)</p>
</li>
</ul>
<h3 id="heading-why-this-approach-beats-planning-first">Why This Approach Beats "Planning First"</h3>
<p><strong>Real feedback trumps theoretical planning.</strong> A working prototype reveals problems no design document can predict. You discover edge cases, UX friction, and technical constraints within days - not months.</p>
<p><strong>Momentum is contagious.</strong> Teams and stakeholders see progress, not promises. That energy compounds.</p>
<p><strong>Technical assumptions get validated early.</strong> Does the virtual file system actually work under load? Does the preview render correctly across browsers? You know immediately.</p>
<blockquote>
<p>💡 <strong>Key Insight:</strong> Many projects fail in endless planning phases. Starting with something tangible - even imperfect - creates a foundation for meaningful iteration (repeated improvement cycles).</p>
</blockquote>
<hr />
<h2 id="heading-step-2-security-isnt-a-featureits-the-foundation">Step 2: Security Isn't a Feature—It's the Foundation</h2>
<p>Here's where most developers get it wrong.</p>
<p>Less than <strong>24 hours</strong> after my initial commit, the second major update focused entirely on <strong>hardening the application for real-world use.</strong></p>
<p>Not new features. Security.</p>
<h3 id="heading-what-i-implemented">What I Implemented</h3>
<p><strong>Rate limiting</strong> (controlling request frequency to prevent abuse):</p>
<ul>
<li><p>3 sign-up attempts per hour</p>
</li>
<li><p>5 login attempts per 15 minutes</p>
</li>
<li><p>10 API requests per hour for anonymous users</p>
</li>
</ul>
<p><strong>Input validation</strong> (checking that data matches expected formats) everywhere:</p>
<ul>
<li><p>Email format verification</p>
</li>
<li><p>Password strength requirements (8+ characters, mixed case, numbers)</p>
</li>
<li><p>Path traversal prevention (blocking attempts to access unauthorized files)</p>
</li>
</ul>
<p><strong>Graceful degradation</strong> (the app still works even when things go wrong):</p>
<ul>
<li><p>Demo mode for users without API keys</p>
</li>
<li><p>Clear error messages instead of cryptic failures</p>
</li>
</ul>
<p><strong>File system security limits:</strong></p>
<ul>
<li><p>Maximum 100 files per project</p>
</li>
<li><p>500KB per file limit</p>
</li>
<li><p>5MB total storage cap</p>
</li>
<li><p>Whitelist of allowed file extensions (only .js, .jsx, .css, etc.)</p>
</li>
</ul>
<h3 id="heading-why-this-matters-more-than-new-features">Why This Matters More Than New Features</h3>
<p>Security vulnerabilities discovered after launch are <strong>exponentially more expensive</strong> to fix. A breach in week one destroys user trust before you've built any.</p>
<blockquote>
<p>🔐 <strong>Red Flag for Clients:</strong> When evaluating development partners, ask "When in your process do you address security?" If the answer is "Phase 3" or "after launch," reconsider.</p>
</blockquote>
<hr />
<h2 id="heading-step-3-documentation-that-actually-helps-meet-claudemd">Step 3: Documentation That Actually Helps (Meet CLAUDE.md)</h2>
<p>Most documentation describes <strong>what exists.</strong></p>
<p>Good documentation describes <strong>why things exist and how they connect.</strong></p>
<p>I created a <a target="_blank" href="https://github.com/nocodeanish/react-ui-generator/blob/main/CLAUDE.md"><code>CLAUDE.md</code></a> file—a living reference document specifically designed for AI-assisted development. This file gives Claude Code (and any developer) the context needed to make intelligent decisions aligned with the project's architecture.</p>
<h3 id="heading-how-documentation-evolved">How Documentation Evolved</h3>
<p><strong>Version 1 (Day 1):</strong> Basic commands and high-level architecture</p>
<ul>
<li><p>100 lines of essential context</p>
</li>
<li><p>Data flow diagrams in plain text</p>
</li>
<li><p>Database schema (structure) documentation</p>
</li>
</ul>
<p><strong>Version 2 (Day 2):</strong> Security features and production guidance</p>
<ul>
<li><p>Demo mode explanation</p>
</li>
<li><p>Rate limiting specifics</p>
</li>
<li><p>Security feature checklist</p>
</li>
</ul>
<p><strong>Version 3 (Day 2-3):</strong> Multi-provider architecture</p>
<ul>
<li><p>Documentation for five AI providers</p>
</li>
<li><p>Encryption methods for user API keys</p>
</li>
<li><p>Complete API endpoint (access point) reference</p>
</li>
</ul>
<h3 id="heading-example-good-vs-bad-documentation">Example: Good vs. Bad Documentation</h3>
<p>❌ <strong>Bad:</strong> "VirtualFileSystem class in file-system.ts"</p>
<p>✅ <strong>Good:</strong> "The unique architecture centers on a virtual file system—components live in memory, not on disk. This enables instant file operations without I/O (input/output delays), live preview updates, user experimentation without filesystem changes, and atomic save operations (all-or-nothing database writes) to the database."</p>
<p>The second version tells you <strong>intent</strong>, not just location. New team members understand philosophy. AI assistants make aligned suggestions. Future maintainers don't accidentally break architectural assumptions.</p>
<hr />
<h2 id="heading-step-4-adding-capabilities-without-breaking-things">Step 4: Adding Capabilities Without Breaking Things</h2>
<p>The third day brought a major expansion: <strong>multi-provider support.</strong></p>
<p>I went from one AI provider (Anthropic) to five:</p>
<ul>
<li><p><strong>Anthropic Claude</strong> (Sonnet, Haiku, Opus)</p>
</li>
<li><p><strong>OpenAI</strong> (GPT-4o, GPT-4o Mini, GPT-4 Turbo)</p>
</li>
<li><p><strong>Google AI</strong> (Gemini 2.0 Flash, Gemini 1.5 Pro/Flash)</p>
</li>
<li><p><strong>OpenRouter</strong> (multi-provider access)</p>
</li>
<li><p><strong>xAI</strong> (Grok 2, Grok 2 Mini)</p>
</li>
</ul>
<p>This wasn't a rewrite. It was a <strong>careful extension.</strong></p>
<h3 id="heading-the-extension-strategy">The Extension Strategy</h3>
<p><strong>1. Create abstractions before adding options</strong></p>
<p>I introduced a provider registry (a central configuration system) that defines each provider's name, models, and settings. Adding a sixth provider later? Trivial.</p>
<p><strong>2. Encrypt user data properly</strong></p>
<p>Users can bring their own API keys. These get stored with <strong>AES-256-GCM encryption</strong> (military-grade encryption standard). The encryption key derives from the JWT secret (authentication token) using <strong>scrypt</strong> (a memory-hard algorithm that's expensive to crack).</p>
<p>Even if someone compromises the database, they can't read API keys.</p>
<p><strong>3. Per-project flexibility</strong></p>
<p>Each project can use different AI providers and models. Power users get flexibility. Casual users get sensible defaults.</p>
<p><strong>4. Graceful fallbacks</strong></p>
<p>The priority chain: environment variable → user key → demo mode.</p>
<p>The application <strong>always works</strong>, even when configurations are incomplete.</p>
<h3 id="heading-what-this-demonstrates">What This Demonstrates</h3>
<p>Successful software evolution doesn't mean constant rebuilding. It means <strong>designing with extension points from the beginning</strong>, then using them when needs emerge.</p>
<hr />
<h2 id="heading-step-5-the-user-experience-layer">Step 5: The User Experience Layer</h2>
<p>Technical correctness isn't enough. The experience must <strong>feel right.</strong></p>
<p>Throughout development, user-facing improvements happened alongside technical work:</p>
<p><strong>Human-readable messages:</strong> Technical tool names like <code>str_replace_editor</code> became "Creating App.jsx"</p>
<p><strong>Theme support:</strong> Dark and light modes for user comfort (and eye health during late-night coding sessions)</p>
<p><strong>Project management:</strong> Rename, delete, and organize projects—features that seem obvious but make the difference between a prototype and a real tool</p>
<p><strong>Thoughtful empty states:</strong> A centered, welcoming interface instead of a blank void when starting fresh</p>
<p>These aren't "nice to have." They're the difference between software users <strong>tolerate</strong> and software users <strong>enjoy.</strong></p>
<hr />
<h2 id="heading-step-6-testing-for-confident-change">Step 6: Testing for Confident Change</h2>
<p>By project completion: <strong>252 tests across multiple test files.</strong></p>
<p>But the number matters less than the <strong>coverage strategy:</strong></p>
<ul>
<li><p><strong>Unit tests</strong> for core utilities (virtual file system, JSX transformation)</p>
</li>
<li><p><strong>Integration tests</strong> for contexts that coordinate multiple systems</p>
</li>
<li><p><strong>Component tests</strong> for user-facing behavior</p>
</li>
</ul>
<p>Each test file lives alongside the code it tests (<a target="_blank" href="https://github.com/nocodeanish/react-ui-generator"><code>__tests__</code> directories</a>). This makes adding tests when adding features a natural habit—not an afterthought.</p>
<p><strong>The payoff:</strong> When multi-provider support was added, existing tests immediately caught regressions (unintended breakages) in the original single-provider behavior.</p>
<blockquote>
<p>🧪 <strong>Testing Philosophy:</strong> Tests aren't about catching bugs. They're about enabling confident change. Good test coverage means you can refactor (restructure code without changing behavior) fearlessly.</p>
</blockquote>
<hr />
<h2 id="heading-the-tech-stack-that-made-it-possible">The Tech Stack That Made It Possible</h2>
<p>For the technically curious, here's what powers the application:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Layer</td><td>Technology</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Frontend</strong></td><td>React 19, Next.js 15 (App Router), TypeScript</td></tr>
<tr>
<td><strong>Styling</strong></td><td>Tailwind CSS v4, Radix UI, next-themes</td></tr>
<tr>
<td><strong>AI Providers</strong></td><td>Anthropic Claude, OpenAI GPT-4o, Google Gemini, OpenRouter, xAI Grok</td></tr>
<tr>
<td><strong>AI Integration</strong></td><td>Vercel AI SDK (unified interface for all providers)</td></tr>
<tr>
<td><strong>Database</strong></td><td>Prisma ORM with SQLite</td></tr>
<tr>
<td><strong>Security</strong></td><td>AES-256-GCM encryption, scrypt key derivation, JWT auth</td></tr>
<tr>
<td><strong>Code Editor</strong></td><td>Monaco Editor with Babel standalone (JSX transform)</td></tr>
<tr>
<td><strong>Testing</strong></td><td>Vitest, React Testing Library</td></tr>
</tbody>
</table>
</div><p>The choice of <strong>Vercel AI SDK</strong> was critical—it provides a unified interface for multiple AI providers, which made adding new providers trivial rather than a major refactor.</p>
<hr />
<h2 id="heading-key-takeaways-for-developers">Key Takeaways for Developers</h2>
<ol>
<li><p><strong>Start with working software.</strong> Iterate from something real, not from specifications.</p>
</li>
<li><p><strong>Security early, security always.</strong> Rate limiting, input validation, and secure data storage aren't premium features—they're baseline expectations.</p>
</li>
<li><p><strong>Document the why, not just the what.</strong> Future you (and your team) will thank present you.</p>
</li>
<li><p><strong>Design for extension.</strong> Abstractions like provider registries make future changes straightforward.</p>
</li>
<li><p><strong>User experience is code quality.</strong> Clean internals that produce confusing interfaces aren't actually clean.</p>
</li>
</ol>
<hr />
<h2 id="heading-key-takeaways-for-clients-and-decision-makers">Key Takeaways for Clients and Decision-Makers</h2>
<p>When evaluating development approaches or partners, ask:</p>
<p><strong>How quickly do they produce working software?</strong> Plans are cheap; working systems are evidence of capability.</p>
<p><strong>When do they address security?</strong> If security is a "phase 3" concern, that's a red flag.</p>
<p><strong>How do they handle documentation?</strong> Good documentation suggests good thinking about systems and their users.</p>
<p><strong>How do they manage complexity?</strong> Adding five AI providers without rewriting the application demonstrates architectural foresight.</p>
<p><strong>Do they ship incrementally?</strong> Six focused commits over three days shows discipline. Sixty commits or three months of silence both suggest problems.</p>
<hr />
<h2 id="heading-the-bottom-line">The Bottom Line</h2>
<p>Building production-ready software with Claude Code isn't about letting AI do everything. It's about <strong>human-AI collaboration</strong>—maintaining discipline while leveraging AI to accelerate execution.</p>
<p>This project evolved from a single-provider prototype to a multi-provider, security-hardened application with comprehensive documentation and 252 tests.</p>
<p><strong>What Claude Code excelled at:</strong></p>
<ul>
<li><p>Scaffolding boilerplate code quickly</p>
</li>
<li><p>Implementing security patterns correctly</p>
</li>
<li><p>Writing comprehensive tests</p>
</li>
<li><p>Maintaining consistency across files</p>
</li>
</ul>
<p><strong>What still required human judgment:</strong></p>
<ul>
<li><p>Architectural decisions</p>
</li>
<li><p>Security priorities</p>
</li>
<li><p>User experience choices</p>
</li>
<li><p>When to stop adding features</p>
</li>
</ul>
<p>The techniques aren't secret:</p>
<ul>
<li><p>Start with something working</p>
</li>
<li><p>Prioritize security</p>
</li>
<li><p>Document thoughtfully (CLAUDE.md!)</p>
</li>
<li><p>Extend rather than rewrite</p>
</li>
<li><p>Never forget that real humans will use what you build</p>
</li>
</ul>
<p><strong>The difference between amateur and professional development isn't talent—it's consistency in applying these principles, project after project.</strong></p>
<hr />
<h2 id="heading-get-the-code">📦 Get the Code</h2>
<p>The entire project is <strong>open source</strong> and available for you to use, learn from, or build upon:</p>
<p><strong>🔗 GitHub Repository:</strong> <a target="_blank" href="https://github.com/nocodeanish/react-ui-generator">github.com/nocodeanish/react-ui-generator</a></p>
<ul>
<li><p>⭐ Star the repo if you find it useful</p>
</li>
<li><p>🍴 Fork it to build your own version</p>
</li>
<li><p>🐛 Open issues for bugs or feature requests</p>
</li>
<li><p>🤝 PRs welcome!</p>
</li>
</ul>
<hr />
<p><em>Built with Claude Code, iterative development, and a commitment to shipping software that actually works.</em></p>
<hr />
<h2 id="heading-connect-amp-continue-the-conversation">Connect &amp; Continue the Conversation</h2>
<p>Found this useful? I'd love to hear about your development process. What's your approach to balancing speed with security?</p>
<p>☕ <a target="_blank" href="https://buymeacoffee.com/anish3592"><strong>Buy me a coffee</strong></a> if this helped you!</p>
<p><strong>Tags:</strong> #claudecode #ai #react #nextjs #typescript #webdev #programming #tutorial #fullstack #anthropic #aitools #softwaredevelopment</p>
]]></content:encoded></item><item><title><![CDATA[How to Implement TOTP(time-based one-time passwords) based 2FA in Bubble.io Without Using Any APIs?]]></title><description><![CDATA[TL;DR
Bubble.io doesn’t natively support TOTP-based two-factor authentication (2FA) - the kind used by Google Authenticator and Authy - because it lacks built-in tools for generating secret keys, verifying time-based codes, and performing cryptograph...]]></description><link>https://anishgandhi.com/how-to-implement-totp-time-based-one-time-passwords-based-2fa-in-bubbleio-without-using-any-api</link><guid isPermaLink="true">https://anishgandhi.com/how-to-implement-totp-time-based-one-time-passwords-based-2fa-in-bubbleio-without-using-any-api</guid><category><![CDATA[bubble.io]]></category><category><![CDATA[No Code]]></category><category><![CDATA[2FA]]></category><category><![CDATA[totp]]></category><category><![CDATA[authentication]]></category><category><![CDATA[Security]]></category><category><![CDATA[google authenticator]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Mon, 20 Oct 2025 16:26:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760974250652/72b8da83-afd9-4b71-bd2e-bac2d0906911.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<h2 id="heading-tldr">TL;DR</h2>
<p><a target="_blank" href="http://Bubble.io">Bubble.io</a> doesn’t natively support TOTP-based two-factor authentication (2FA) - the kind used by Google Authenticator and Authy - because it lacks built-in tools for generating secret keys, verifying time-based codes, and performing cryptographic operations.</p>
<p>To overcome this, you can use the Crazy Two Factor – Authentication Plugin, which enables you to:<br />✅ Generate and verify TOTP codes directly in Bubble<br />✅ Display QR codes for users to scan with any authenticator app<br />✅ Implement full 2FA flows (enable, verify, login) without any external APIs</p>
<p>This plugin-based approach keeps everything inside your Bubble app — making it secure, efficient, and easy to maintain, while providing a professional-grade authentication experience comparable to SaaS-level systems.</p>
</blockquote>
<p>This article is Powered by <a target="_blank" href="https://zeroic.in/">Zeroic</a> and Sponsored by <a target="_blank" href="https://www.formulabot.com/">FormulaBot</a>.</p>
<h2 id="heading-limitations-of-bubbleiohttpbubbleios-native-2fa">Limitations of <a target="_blank" href="http://Bubble.io">Bubble.io</a>’s native 2FA:</h2>
<p>Bubble.io’s built-in <strong>two-factor authentication (2FA)</strong> primarily revolves around sending <strong>verification codes via email or SMS</strong>. While this works for basic identity verification, it doesn’t cover the <strong>TOTP (Time-Based One-Time Password)</strong> standard - the system used by popular authentication apps like <strong>Google Authenticator</strong>, <strong>Authy</strong>, or <strong>Microsoft Authenticator</strong>.</p>
<p>TOTP works differently from email or SMS verification. Instead of sending a code over the internet or through a messaging service, it relies on a <strong>shared secret key</strong> that’s stored both on the server (Bubble app) and the user’s device (in the authenticator app). This key generates a new 6-digit code every 30 seconds <strong>entirely offline</strong>, meaning no communication between your app and the authenticator is needed during verification.</p>
<p>Here’s the challenge:<br />Bubble’s <strong>native user authentication system</strong> doesn’t provide:</p>
<ul>
<li><p>A way to generate and share the secret key securely with the user.</p>
</li>
<li><p>Built-in tools for generating or verifying TOTP codes.</p>
</li>
<li><p>Access to low-level cryptographic functions (like HMAC-SHA1) is needed to calculate time-based tokens.</p>
</li>
</ul>
<p>Because of these missing components, developers <strong>can’t directly implement app-based verification</strong> like TOTP purely through Bubble’s native features.</p>
<p>This limitation makes it difficult to offer the same <strong>security and convenience</strong> that users expect from standard authenticator-based 2FA systems - where verification doesn’t depend on email delivery, SMS reliability, or network access.</p>
<h2 id="heading-advantages-of-using-the-plugin-over-the-api">Advantages of using the Plugin over the API</h2>
<p>Implementing TOTP (Time-Based One-Time Password) authentication manually inside Bubble can be quite complex. You’d need to handle several technical layers yourself - like generating the secret key, creating a QR code for users to scan with Google Authenticator, calculating time-based codes using cryptographic algorithms (HMAC-SHA1), and then verifying those codes securely when the user logs in.</p>
<p>While it’s possible to achieve some of this using custom JavaScript or backend workflows, the process is <strong>time-consuming, error-prone, and security-sensitive</strong>. That’s where a <strong>dedicated plugin</strong> comes in handy.</p>
<p>A good plugin encapsulates all these technical details behind simple Bubble actions - so you can set up TOTP-based 2FA with just a few workflows instead of writing and testing custom code.</p>
<p>Another big advantage is that you <strong>don’t need to register your app with third-party authentication providers</strong> like Auth0 or Stytch. These platforms require API keys, account setup, and extra configuration steps - and in many cases, their APIs come with usage limits or ongoing subscription costs.</p>
<p>By using a <strong>Bubble-native plugin</strong>, your entire 2FA system remains self-contained inside your Bubble app. This means:</p>
<ul>
<li><p><strong>No dependency on external APIs or services</strong>.</p>
</li>
<li><p><strong>Faster setup</strong> - everything happens inside Bubble.</p>
</li>
<li><p><strong>Lower long-term maintenance</strong>, since you’re not managing third-party credentials or webhooks.</p>
</li>
</ul>
<p>So, as a plugin has a one-time fee, it often <strong>pays for itself</strong> quickly in saved development hours, reduced complexity, and a cleaner, more maintainable authentication setup.</p>
<p>Here, I will provide a step-by-step guide to implement <strong>TOTP</strong>(time-based one-time passwords) <strong>based two-factor authentication (2FA) that will be</strong> compatible with popular authenticator apps such as Google Authenticator, Authy Authenticator, and Microsoft Authenticator.</p>
<h2 id="heading-step-1-install-crazy-two-factor-authentication-plugin-in-the-bubbleio-app">Step 1: Install Crazy Two Factor - Authentication Plugin in the bubble.io app</h2>
<p>You can find the plugin <a target="_blank" href="https://bubble.io/plugin/crazy-two-factor---authentication-1689744568401x402948793577766900">here</a>.</p>
<h2 id="heading-step-2-setup-totp-based-2fa-enabler-for-user-in-bubbleio-app">Step 2: Setup TOTP based 2FA Enabler for User in bubble.io app</h2>
<p>Here, I have set this up on the user’s profile page, where the user can enable 2fa with switch toggle.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760969155928/e7dc0cfc-9259-422b-81d1-1f5ba7b05d66.png" alt class="image--center mx-auto" /></p>
<p>Once the switch is Yes, There is an action named ‘Two Factor’ from plugin, Use it to enable 2fa. Enabling 2fa won’t require secret and verification token as you can see in the screenshot below. You can write any text as an app name. This name will be shown in your authenticator app.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760969740050/f558439b-90ce-45a5-8aad-d717e2b3491b.png" alt class="image--center mx-auto" /></p>
<p>Now create 4 new field’s in User table named secret, is-2fa-enabled, is-2fa-verified and is-2fa-qr-code. All this field will be used in next step.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760969978882/9b269d3d-4776-4e20-ab1d-042214177f9c.png" alt class="image--center mx-auto" /></p>
<p>So bascially this ‘Two Factor’ action is generating QR code and secret which will be used to enable TOTP based 2fa for user. is-2fa-verified is ‘no’ because user just have enabled it but verification is still pending.</p>
<h2 id="heading-step-3-setup-totp-based-2fa-verifier-for-user-in-bubbleio-app">Step 3: Setup TOTP based 2FA Verifier for User in bubble.io app</h2>
<p>Now Create a Group where this 2fa-qr-code will be shown and User will be asked to scan the QR code in authenticator app such as Google Authenticator and Write 6 digit token number in input box to verify as shown in the image below. This group will be shown immediately once the is-2fa-enabled is ‘Yes’.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760970697778/e6939c59-d968-4fd6-baf6-6871e1f173c9.png" alt class="image--center mx-auto" /></p>
<p>Once the 6 digit token is provided and verification button is clicked, Run ‘Two factor’ action again. This time, it will be to verify and not to enable. Here is the setup image below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760970911255/aa13621d-1170-4e4e-9be0-b2065bd8cbe6.png" alt class="image--center mx-auto" /></p>
<p>Now result of this action is either error or return data. If the result’s error detail is not empty → Show error to user below QR code or wherever you want.<br />If the result’s Return data is not empty→ Make is-2fa-verified ‘Yes’. Check the image below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760971464571/58561ec7-529e-4185-99f3-99fefd23635e.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-4-setup-privacy-rules-for-user-table">Step 4: Setup Privacy rules for User table</h2>
<p>Now, all the fields of user table must be protected by privacy rules. So logged out user’s details won’t be visible in network calls or concole app file. But I will make user table details find in searches enabled. Basically When will make ‘Do a search for user’ call, it will give me result but data will be empty because I can find the user in search but data is privacy protected.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760972243740/09ba7237-9222-4fb0-b66a-39af7c3012d2.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-5-setup-login-mechanism-for-2fa-verifiedyes-user">Step 5: Setup login mechanism for 2fa-verified=Yes User</h2>
<p>Once the user writes email and password on login page, check whether user has 2fa-enabled or not. I am checking it using custom event and returning data as yes or no.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760972647240/fc1f3c6c-d6f0-4f2d-9c7a-2468ef5fb314.png" alt class="image--center mx-auto" /></p>
<p>If the search result is empty, the return data will be no and If the search reult is not empty, the return data will be Yes.<br />If the return data is yes, then on click of login, don’t ‘Log the User In’ but show verify group first ask for 6 digit verification token.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760972782155/1a0f6592-f513-477c-9ac5-989f3adb96a2.png" alt class="image--center mx-auto" /></p>
<p>Once the input is not empty, Allow user to click continue button.<br />Log the user in and verify 6 digit verification code using ‘Two Factor’ action.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760973021176/3772bd99-87fb-45e4-a34b-aa5e99f8d0eb.png" alt class="image--center mx-auto" /></p>
<p>If the result of the ‘Two Factor’ action has an error detail not empty → Log the user out immediately.<br />If the result of the ‘Two Factor’ action has result data not empty → Keep user logged in.</p>
<p>This approach not only simplifies the setup process but also maintains the entire authentication system within the Bubble environment, reducing dependencies on third-party services and minimizing long-term maintenance. Ultimately, this method provides a more secure, efficient, and user-friendly authentication experience.</p>
<h2 id="heading-key-takeaways-implementing-totp-based-2fa-in-bubbleio">Key Takeaways: Implementing TOTP-Based 2FA in Bubble.io</h2>
<ul>
<li><p><strong>Bubble.io’s native 2FA</strong> supports only email and SMS verification - not TOTP-based authenticator apps.</p>
</li>
<li><p><strong>TOTP (Time-Based One-Time Password)</strong> works completely offline and generates new 6-digit codes every 30 seconds using apps like Google Authenticator, Authy, or Microsoft Authenticator.</p>
</li>
<li><p><strong>Bubble lacks native cryptographic functions</strong> like HMAC-SHA1, which are required to generate and verify time-based one-time passwords.</p>
</li>
<li><p><strong>The Crazy Two Factor – Authentication Plugin</strong> provides a simple and secure solution to enable TOTP 2FA inside Bubble without using any external APIs.</p>
</li>
<li><p>The plugin handles all critical steps - secret key generation, QR code creation, and token verification - directly within Bubble workflows.</p>
</li>
<li><p><strong>Advantages of using the plugin:</strong></p>
<ul>
<li><p>No dependency on external APIs like Auth0 or Stytch</p>
</li>
<li><p>Faster and simpler setup</p>
</li>
<li><p>Lower maintenance and better control over security</p>
</li>
</ul>
</li>
<li><p>Developers can easily add workflows to <strong>enable, verify, and enforce 2FA</strong> for users with just a few actions.</p>
</li>
<li><p>Applying <strong>strong privacy rules</strong> in the User data type ensures user secrets remain secure.</p>
</li>
<li><p>This method keeps the <strong>entire authentication system Bubble-native</strong>, providing a robust, user-friendly, and scalable security flow.</p>
</li>
</ul>
<h2 id="heading-frequently-asked-questions-faqs">Frequently asked questions (FAQs)</h2>
<h3 id="heading-q-what-is-totp-based-2fa"><strong>Q:</strong> What is TOTP-based 2FA?</h3>
<p>A: TOTP (Time-Based One-Time Password) is a form of two-factor authentication where a unique 6-digit code is generated every 30 seconds by an authenticator app like Google Authenticator, Authy, or Microsoft Authenticator. It’s more secure than SMS or email-based 2FA because it works entirely offline and cannot be intercepted.</p>
<h3 id="heading-q-does-bubbleio-natively-support-totp-based-2fa"><strong>Q:</strong> Does Bubble.io natively support TOTP-based 2FA?</h3>
<p>A: No, Bubble.io’s native authentication system only supports email or SMS verification codes. It doesn’t include built-in support for TOTP-based authentication or cryptographic tools like HMAC-SHA1 needed to generate time-based tokens.</p>
<h3 id="heading-q-why-not-just-use-bubbles-built-in-2fa-feature"><strong>Q:</strong> Why not just use Bubble’s built-in 2FA feature?</h3>
<p>A: Bubble’s native 2FA is fine for simple verification but lacks flexibility and security features like offline token generation, authenticator app compatibility, and customizable verification flows. TOTP-based 2FA offers stronger security and a smoother user experience.</p>
<h3 id="heading-q-can-i-build-totp-based-2fa-in-bubble-without-using-any-apis"><strong>Q:</strong> Can I build TOTP-based 2FA in Bubble without using any APIs?</h3>
<p>A: Yes, you can! By using the <strong>Crazy Two Factor – Authentication Plugin</strong>, you can implement a fully functional, app-based TOTP 2FA system without integrating external APIs like Auth0 or Stytch. The plugin handles secret generation, QR code creation, and verification directly within Bubble.</p>
<h3 id="heading-q-is-the-plugin-better-than-using-an-api"><strong>Q:</strong> Is the plugin better than using an API?</h3>
<p>A: Yes, for most Bubble developers. The plugin simplifies setup, avoids third-party dependencies, and keeps your entire authentication system within the Bubble ecosystem. APIs like Auth0 add configuration overhead, require account registration, and often charge usage-based fees.</p>
<h3 id="heading-q-is-the-plugin-secure"><strong>Q:</strong> Is the plugin secure?</h3>
<p>A: Yes. The plugin uses secure key generation and verification methods that align with standard TOTP algorithms. However, you should still apply <strong>strict privacy rules</strong> to your User data type to protect sensitive fields like <code>secret</code> and <code>is-2fa-verified</code>.</p>
<h3 id="heading-q-can-users-disable-2fa-once-enabled"><strong>Q:</strong> Can users disable 2FA once enabled?</h3>
<p>A: Yes. You can add a simple workflow that resets the user’s <code>is-2fa-enabled</code>, <code>is-2fa-verified</code>, and <code>secret</code> fields to blank values. This allows users to turn off 2FA when needed.</p>
<h3 id="heading-q-does-this-work-with-all-authenticator-apps"><strong>Q:</strong> Does this work with all authenticator apps?</h3>
<p>A: As per plugin creator, It's a JavaScript library designed for generating and verifying One-Time Passwords (OTPs). The plugin is specifically compatible with popular authenticator apps such as:<br />- Google Authenticator<br />- Authy Authenticator<br />- Microsoft Authenticator</p>
<p><strong>Formula Bot has sponsored this article - Formula Bot is your AI-powered data analyst that instantly transforms data into charts, insights, reports, and more.</strong> <a target="_blank" href="https://www.formulabot.com/"><strong>Here is the link to check it out.</strong></a></p>
<p><strong>This article is powered by the dev team behind FormulaBot - Zeroic, a low-code and AI development studio.</strong> <a target="_blank" href="https://zeroic.in/"><strong>Here is the link to check them out.</strong></a></p>
]]></content:encoded></item><item><title><![CDATA[How to Migrate a Relational Database Between Two Bubble.io Apps Using n8n (Step-by-Step)]]></title><description><![CDATA[TL;DR

Problem: Move parents (data_type_1) and children (data_type_2) between Bubble apps while preserving the relation (parent list of children & child’s single parent).

Approach: Read all parents from App-1, compare with App-2, create missing pare...]]></description><link>https://anishgandhi.com/how-to-migrate-a-relational-database-between-two-bubbleio-apps-using-n8n-step-by-step</link><guid isPermaLink="true">https://anishgandhi.com/how-to-migrate-a-relational-database-between-two-bubbleio-apps-using-n8n-step-by-step</guid><category><![CDATA[data migration]]></category><category><![CDATA[datamigration]]></category><category><![CDATA[data migration tools]]></category><category><![CDATA[n8n]]></category><category><![CDATA[bubble.io]]></category><category><![CDATA[bubble]]></category><category><![CDATA[automation]]></category><category><![CDATA[automation tools]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Tue, 14 Oct 2025 06:16:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760422353031/7ed36483-b00c-428c-bfc7-8dadda888897.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<h1 id="heading-tldr">TL;DR</h1>
<ul>
<li><p>Problem: Move parents (<code>data_type_1</code>) and children (<code>data_type_2</code>) between Bubble apps while preserving the relation (parent list of children &amp; child’s single parent).</p>
</li>
<li><p>Approach: Read all parents from App-1, compare with App-2, create missing parents in App-2, fetch each parent’s children from App-1, create children in App-2, then patch parent records in App-2 to reference created children IDs.</p>
</li>
<li><p>Tool: n8n workflow (manual trigger → HTTP GET App1/App2 → dedupe → batch create → create children → merge → update relationships).</p>
</li>
</ul>
</blockquote>
<h2 id="heading-challenges-in-relational-database-migration">Challenges in relational database migration</h2>
<ul>
<li><p><strong>Duplicate prevention</strong> - avoid creating the same parent twice in App-2.</p>
</li>
<li><p><strong>Unique ID mapping</strong> - Bubble object <code>_id</code> values differ between apps; you must map App-1 IDs → App-2 IDs to reconstruct relations.</p>
</li>
<li><p><strong>List vs single fields</strong> - parent often stores a <em>list</em> of child IDs, child stores a <em>single</em> parent reference. Both directions must be kept consistent.</p>
</li>
<li><p><strong>API constraints &amp; rate limits</strong> - Bubble APIs can be rate-limited; batch and sleep strategically.</p>
</li>
<li><p><strong>Data shape differences</strong> - field names (or types) may differ; must transform payloads.</p>
</li>
<li><p><strong>Atomicity</strong> - multi-step migrations can partially succeed; plan for resume/rollback (store mapping and logs).</p>
</li>
</ul>
<h2 id="heading-why-use-n8n-for-data-migration">Why use n8n for data migration?</h2>
<ul>
<li><p><strong>No-code glue</strong>: HTTP nodes + code nodes let you call the Bubble REST API and transform payloads visually.</p>
</li>
<li><p><strong>Batched processing</strong>: the splitInBatches node handles large datasets safely and avoids rate-limit bursts.</p>
</li>
<li><p><strong>Custom logic</strong>: Code nodes for dedupe/formatting relations are simple JS.</p>
</li>
<li><p><strong>Visibility &amp; re-run</strong>: You can re-run failed batches and inspect inputs/outputs.</p>
</li>
<li><p><strong>Extendability</strong>: Same pattern works to migrate between Bubble ↔ Supabase ↔ Xano, etc (just change endpoints/auth and payload shape).</p>
</li>
</ul>
<h2 id="heading-pre-requisite-setup-in-bubble-what-to-prepare-before-running-the-workflow">Pre-requisite setup in Bubble (what to prepare before running the workflow)</h2>
<ul>
<li><p><strong>Admin API keys (or API token) for both apps</strong>:</p>
<ul>
<li>Create API keys / set the proper endpoint access in Bubble (Settings → API if required). Ensure the token has create/read/patch permissions for the data types in question.</li>
</ul>
</li>
<li><p><strong>Enable Data API on both apps</strong> (Settings → API → check <strong>Expose Data API</strong> for each Data Type you’ll use).</p>
</li>
<li><p><strong>CORS / IP</strong> - If you restrict IPs, add the n8n server IP or run n8n where it can reach Bubble.</p>
</li>
<li><p><strong>Field mapping document</strong> - record field names in App-1 and App-2 for each data_type. (e.g., <code>field_1</code>, <code>_variants_list_custom_data_type_2</code>, <code>field_name_text</code>, etc.) Sample n8n JSON references names - confirm they match your bubble types.</p>
</li>
<li><p><strong>Backup</strong> - export both data sets (Data → App Data → Export) before running the migration.</p>
</li>
<li><p><strong>Test environment</strong> - always run on staging apps first.</p>
</li>
<li><p><strong>Decide mapping storage</strong> - either keep mapping in n8n run data or write a temporary mapping table in App-2 (recommended for large runs).</p>
</li>
</ul>
<h2 id="heading-n8n-setup-for-data-migration-explained-with-an-example">n8n Setup for data migration explained with an example</h2>
<p>Use <a target="_blank" href="https://drive.google.com/file/d/15iDaVUO4FLMNXnDaNQUMW4xpgtzrmoIL/view?usp=drive_link">this JSON file</a> as a blueprint</p>
<p>Note: replace <a target="_blank" href="http://app1domain.com"><code>app1domain.com</code></a> and <a target="_blank" href="http://app2domain.com"><code>app2domain.com</code></a> With your actual Bubble app API hostnames and set the credentials referenced in the JSON.</p>
<h3 id="heading-1-manual-trigger">1) Manual trigger</h3>
<ul>
<li><p><strong>Node:</strong> <code>When clicking ‘Execute workflow’</code> (manualTrigger)</p>
</li>
<li><p>Purpose: Start the migration manually so you can control the run.</p>
</li>
</ul>
<h3 id="heading-2-get-all-parents-from-app-1">2) Get all parents from App-1</h3>
<ul>
<li><p><strong>Node:</strong> <code>Get data_type_1 data - App 1</code> (HTTP Request GET → <a target="_blank" href="https://app1domain.com/api/1.1/obj/data_type_1"><code>https://app1domain.com/api/1.1/obj/data_type_1</code></a>)</p>
</li>
<li><p>Important settings:</p>
<ul>
<li><p>Authentication: HTTP Bearer (use your Bubble API token).</p>
</li>
<li><p>Keep default GET, no constraints for first full read (unless you want partial).</p>
</li>
</ul>
</li>
<li><p>Output: JSON response with <code>response.results</code> array.</p>
</li>
</ul>
<h3 id="heading-3-get-all-parents-from-app-2-to-detect-duplicates">3) Get all parents from App-2 (to detect duplicates)</h3>
<ul>
<li><p><strong>Node:</strong> <code>Get data_type_1 data - App 2</code> (HTTP Request GET → <a target="_blank" href="https://app2domain.com/api/1.1/obj/data_type_1"><code>https://app2domain.com/api/1.1/obj/data_type_1</code></a>)</p>
</li>
<li><p>Purpose: get existing records on the destination, so we create only the missing parents.</p>
</li>
</ul>
<h3 id="heading-4-filter-missing-parents-code">4) Filter missing parents (Code)</h3>
<ul>
<li><p><strong>Node:</strong> <code>Filter list of Missing data in app2</code> (Code node - JS)</p>
</li>
<li><p>Code logic (from sampleJSON): it loads App1 &amp; App2 results, extracts <code>field_name_text</code> from App2 and filters App1 for entries not present in App2.</p>
</li>
<li><p>Action: this returns items representing <em>parents that need creation</em> in App-2.</p>
</li>
<li><p><strong>You must verify</strong> the key used to compare uniqueness (<code>field_name_text</code> in the sample workflow). Replace with the real uniqueness field (e.g., <code>slug</code>, <code>email</code>, <code>name</code>) if different.</p>
</li>
</ul>
<p><strong>Snippet used</strong>:</p>
<pre><code class="lang-json">const app1 = $item(<span class="hljs-number">0</span>).$node[<span class="hljs-string">"Get data_type_1 data - App 1"</span>].json.response.results;
const app2 = $item(<span class="hljs-number">0</span>).$node[<span class="hljs-string">"Get data_type_1 data - App 2"</span>].json.response.results;
const app2Ids = app2.map(t =&gt; t.field_name_text);
const missing = app1.filter(t =&gt; !app2Ids.includes(t.field_name_text));
return missing.map(t =&gt; ({ json: { field_name_text: t.field_name_text } }));
</code></pre>
<h3 id="heading-5-batch-loop-for-creating-missing-parents">5) Batch loop for creating missing parents</h3>
<ul>
<li><p><strong>Node:</strong> <code>loop create data_type_1 in app2</code> (SplitInBatches)</p>
</li>
<li><p>Purpose: process missing parents in small batches. Suggested batch size: 10–50, depending on API limits.</p>
</li>
</ul>
<p><strong>Important</strong>: set <code>batchSize</code> appropriately and use <code>wait between requests</code> (or add a Wait node) to avoid rate limits.</p>
<h3 id="heading-6-for-each-missing-parent-fetch-the-full-parent-from-app-1-and-fetch-its-children">6) For each missing parent, fetch the full parent from App-1 and fetch its children</h3>
<ul>
<li><p><strong>Node (parallel outputs):</strong></p>
<ul>
<li><p><code>Fetch individual data_type_1 from app1</code> - HTTP GET to <a target="_blank" href="https://app1domain.com/api/1.1/obj/data_type_1"><code>https://app1domain.com/api/1.1/obj/data_type_1</code></a> with <code>constraints</code> A query that finds the parent by the uniqueness field. The JSON query expression in the sample workflow is:</p>
<pre><code class="lang-json">  constraints = JSON.stringify([{ key: <span class="hljs-string">"id_name_text"</span>, constraint_type: <span class="hljs-string">"equals"</span>, value: $json.id_name_text }])
</code></pre>
<p>  Replace <code>id_name_text</code> with your actual unique key.</p>
</li>
<li><p><code>Create data_type_1 in app2</code> - HTTP POST to <a target="_blank" href="https://app2domain.com/api/1.1/obj/data_type_1"><code>https://app2domain.com/api/1.1/obj/data_type_1</code></a> with a JSON body mapping the fields from App-1 response to App-2 fields. Sample workflow uses:</p>
<pre><code class="lang-json">  {
    <span class="hljs-attr">"field_1"</span>: {{ $json[<span class="hljs-attr">"response"</span>][<span class="hljs-attr">"results"</span>][0][<span class="hljs-attr">"field_1"</span>] }},
    <span class="hljs-string">"field_2"</span>: <span class="hljs-string">"{{ $json['response']['results'][0]['field_2'] }}"</span>,
    <span class="hljs-string">"list_of_field_3"</span>: {{ JSON.stringify($json[<span class="hljs-attr">"response"</span>][<span class="hljs-attr">"results"</span>][0][<span class="hljs-attr">"list_of_field_3"</span>]) }}
  }
</code></pre>
<p>  Update <code>field_1/field_2/list_of_field_3</code> to match actual fields.</p>
</li>
</ul>
</li>
</ul>
<p><strong>Why are both parallel?</strong> You create the new parent in App-2 and also fetch the parent’s children (next step) - both outputs are merged later.</p>
<h3 id="heading-7-fetch-child-data-for-that-parent-in-app-1">7) Fetch child data for that parent in App-1</h3>
<ul>
<li><p><strong>Node:</strong> <code>Fetch child data (data_type_2) from app1</code> - HTTP GET to <a target="_blank" href="https://app1domain.com/api/1.1/obj/data_type_2"><code>https://app1domain.com/api/1.1/obj/data_type_2</code></a> with a constraint <code>in</code> on parent’s <code>_id</code> list (see sample JSON).</p>
</li>
<li><p>Example query in sample workflow:</p>
<pre><code class="lang-json">  constraints = JSON.stringify([{ key: <span class="hljs-string">"_id"</span>, constraint_type: <span class="hljs-string">"in"</span>, value: $json[<span class="hljs-string">"response"</span>][<span class="hljs-string">"results"</span>][<span class="hljs-number">0</span>][<span class="hljs-string">"_variants_list_custom_data_type_2"</span>] }])
</code></pre>
<p>  Replace the key <code>"_variants_list_custom_data_type_2"</code> with the actual parent → child list field in App-1.</p>
</li>
</ul>
<h3 id="heading-8-merge-parent-create-output-fetched-children">8) Merge parent-create output + fetched children</h3>
<ul>
<li><strong>Node:</strong> <code>Merge</code> - combines the <code>Create data_type_1 in app2</code> output and the <code>Fetch child data</code> output so the workflow has both the new parent’s App-2 result <em>and</em> the list of App-1 children to create.</li>
</ul>
<h3 id="heading-9-create-child-records-in-app-2">9) Create child records in App-2</h3>
<ul>
<li><p><strong>Node:</strong> <code>Create data_type_2 in app2</code> - HTTP POST to <a target="_blank" href="https://app2domain.com/api/1.1/obj/data_type_2"><code>https://app2domain.com/api/1.1/obj/data_type_2</code></a></p>
</li>
<li><p>Payload example from sample JSON:</p>
<pre><code class="lang-json">  {
    <span class="hljs-attr">"_field_1"</span>: $json.id,
    <span class="hljs-attr">"isactive_boolean"</span>: $json[<span class="hljs-string">"response.results"</span>][<span class="hljs-string">"isactive_boolean"</span>],
    <span class="hljs-attr">"field_2"</span>: $json[<span class="hljs-string">"response.results"</span>][<span class="hljs-string">"field_2_text"</span>]
  }
</code></pre>
<ul>
<li><p><code>_field_1</code> should store the parent reference (use the <strong>new parent id returned from App-2</strong> - ensure you capture it).</p>
</li>
<li><p>If the child needs to reference the parent via Bubble relation, send the correct field (usually the parent’s <code>_id</code> or <code>list</code> field format expected by Bubble.</p>
</li>
</ul>
</li>
</ul>
<p><strong>Important:</strong> the code in the sample workflow sets <code>field_id</code> via a Set node, which stores the created record ID for later mapping. Make sure you capture and persist <code>App1_parent_id -&gt; App2_parent_id</code> the mapping somewhere (either in a temporary data type in App-2 or as a file). The <code>Set</code> node in the sample flow does <code>field_id = $</code><a target="_blank" href="http://json.id"><code>json.id</code></a> - Adapt the name to your types. Data Migration from One Bubble …</p>
<h3 id="heading-10-merge-app-2-child-amp-parent-rows-into-relation-format">10) Merge App-2 child &amp; parent rows into relation format</h3>
<ul>
<li><p><strong>Node:</strong> <code>Merge app2 data_type_2 and data_type_1 in single json</code> - combines created child responses and created parent responses into one stream.</p>
</li>
<li><p><strong>Node:</strong> <code>Change format of data for relation</code> (Code node) - the JS groups children by parent id and creates an object like:</p>
<pre><code class="lang-json">  { json: { data_type_1_id: dataType1Id, data_type_2_ids: [ ... ] } }
</code></pre>
<p>  This is exactly what you need to PATCH the parent record in App-2 with the list of child IDs.</p>
</li>
</ul>
<p><strong>Code used in sample workflow</strong> (copy/paste):</p>
<pre><code class="lang-json">const items = $input.all();
const grouped = {};
for (const item of items) {
  const dataType1Id = item.json.data_type_1_id;
  const dataType2 = item.json.data_type_2;
  if (!grouped[dataType1Id]) { grouped[dataType1Id] = new Set(); }
  grouped[dataType1Id].add(dataType2);
}
const output = Object.entries(grouped).map(([dataType1Id, dataType2Set]) =&gt; ({
  json: {
    data_type_1_id: dataType1Id,
    data_type_2_ids: Array.from(dataType2Set),
  }
}));
return output;
</code></pre>
<p>(Modify <code>data_type_1_id</code> / <code>data_type_2</code> keys to match your payload names.)</p>
<h3 id="heading-11-patch-parent-in-app-2-to-update-the-relationship-list-of-children">11) PATCH parent in App-2 to update the relationship (list of children)</h3>
<ul>
<li><p><strong>Node:</strong> <code>Update Relationship between data_tytpe 1 and data_type 2 (App 2)</code> - HTTP PATCH to <a target="_blank" href="https://app2domain.com/api/1.1/obj/data_type_2/"><code>https://app2domain.com/api/1.1/obj/data_type_2/</code></a><code>{{ $</code><a target="_blank" href="http://json.data"><code>json.data</code></a><code>_tyep_1_id}}</code> (note: fix typo <code>data_tyep_1_id</code> → <code>data_type_1_id</code>).</p>
</li>
<li><p>Body example from JSON:</p>
<pre><code class="lang-json">  {
    <span class="hljs-attr">"_variants_list_custom_data_type_2"</span>: $json.variant_ids
  }
</code></pre>
<p>  Replace <code>_variants_list_custom_data_type_2</code> with your parents’ list field name in App-2 and <code>variant_ids</code> with the array produced by the previous code node (e.g., <code>data_type_2_ids</code>).</p>
</li>
</ul>
<p><strong>Important fixes &amp; checks</strong></p>
<ul>
<li><strong>Ensure mapping</strong>: when creating child records in App-2, capture their returned <code>id</code> and attach the created child ID to the mapping item so the code node can group them.</li>
</ul>
<h2 id="heading-practical-tips-gotchas-amp-recommended-config">Practical tips, gotchas &amp; recommended config</h2>
<ul>
<li><p><strong>Batch size</strong>: Use conservative sizes (10) and increase only after testing. Add a small Wait (1s) after each POST if you hit rate limits.</p>
</li>
<li><p><strong>Logging</strong>: store each created mapping row in a temporary Bubble data type <code>migration_mapping</code> with fields <code>app1_id</code>, <code>app2_id</code>, <code>type</code>, <code>status</code>. This helps resume if something fails.</p>
</li>
<li><p><strong>Idempotency</strong>: make API POSTs idempotent by checking uniqueness before creating (as your code does by listing App-2 first). Also, consider creating a unique <code>external_id</code> field to catch duplicates later.</p>
</li>
<li><p><strong>Field type conversions</strong>: arrays/lists must be <code>JSON.stringify(...)</code> in your POST bodies (workflow already does that in examples).</p>
</li>
<li><p><strong>Testing flow</strong>: run with 3–5 sample parents first, inspect results, then full run.</p>
</li>
<li><p><strong>Rollback plan</strong>: if needed, delete the created App-2 records using the stored mapping.</p>
</li>
</ul>
<h2 id="heading-how-does-this-logic-extend-beyond-bubbleio">How does this logic extend beyond Bubble.io?</h2>
<p>The pattern is <strong>generic</strong>:</p>
<ol>
<li><p>Read source rows via API (App-1 / Supabase / Xano).</p>
</li>
<li><p>Read destination existing rows to skip duplicates.</p>
</li>
<li><p>Create missing parents on the destination.</p>
</li>
<li><p>For each parent, fetch children from the source and CREATE them on the destination.</p>
</li>
<li><p>Build a mapping of <code>source_id -&gt; dest_id</code>.</p>
</li>
<li><p>PATCH parents on the destination to attach children IDs.</p>
</li>
</ol>
<h3 id="heading-change-points-when-migrating-to-other-dbs"><strong>Change points when migrating to other DBs</strong>:</h3>
<ul>
<li><p><strong>Auth</strong>: use Bearer/token for Bubble; Supabase uses service_role key; Xano uses its token - n8n HTTP nodes handle each.</p>
</li>
<li><p><strong>Endpoints &amp; payload shapes</strong>: adapt POST/patch body to each API’s contract (Supabase expects POST <code>/rest/v1/&lt;table&gt;</code> with headers <code>apikey</code> + <code>Authorization</code> and <code>Prefer: return=representation</code> to get created IDs).</p>
</li>
<li><p><strong>Batching &amp; bulk insert support</strong>: Supabase supports bulk inserts - use that to speed up child creation (but still capture returned IDs).</p>
</li>
<li><p><strong>Relationship formats</strong>: relational DBs use foreign keys rather than a list of IDs. For relational DBs, you will set a foreign key on the child row to the parent ID (simpler than patching the list on the parent).</p>
</li>
</ul>
<h2 id="heading-frequently-asked-questions-faqs">Frequently asked questions (FAQs)</h2>
<h3 id="heading-q-can-i-migrate-large-datasets-10k-records-with-this-n8n-flow"><strong>Q: Can I migrate large datasets (10k+ records) with this n8n flow?</strong></h3>
<p>A: Yes — but use batching, temporary mapping storage, and run in stages. Monitor rate limits and use <code>splitInBatches</code> with conservative sizes (e.g., 50).</p>
<h3 id="heading-q-how-do-i-ensure-no-duplicates-in-app-2"><strong>Q: How do I ensure no duplicates in App-2?</strong></h3>
<p>A: Compare a unique field (slug/email/external_id) from App-1 with App-2 before creating. Optionally store an <code>external_id</code> On App-2 to mark the origin.</p>
<h3 id="heading-q-do-i-need-to-write-code"><strong>Q: Do I need to write code?</strong></h3>
<p>A: Minimal JS is used inside n8n Code nodes for grouping/deduping - copy/paste snippets provided in this guide. No external scripts required.</p>
<h3 id="heading-q-what-if-field-names-differ-between-apps"><strong>Q: What if field names differ between apps?</strong></h3>
<p>A: Map fields explicitly in the <code>Create</code> POST node bodies; use <code>JSON.stringify()</code> for lists and use Code nodes to reshape data.</p>
<h3 id="heading-q-how-to-resume-a-partially-failed-run"><strong>Q: How to resume a partially failed run?</strong></h3>
<p>A: Keep a <code>migration_mapping</code> (or use the <code>mapping</code> Set node) that stores successful <code>app1_id -&gt; app2_id</code>. Filter already migrated parents out on rerun.</p>
<h3 id="heading-q-is-it-safer-to-create-children-first-or-parent-first"><strong>Q: Is it safer to create children first or parent first?</strong></h3>
<p>A: Create the parent first (to get its App-2 ID), then create children attaching that parent id. After all children are created, patch the parent with a list if your data model requires it.</p>
<h1 id="heading-quick-checklist-before-pressing-execute">Quick checklist before pressing Execute</h1>
<ul>
<li><p>Export &amp; backup both app data.</p>
</li>
<li><p>Confirm API tokens for both App-1 and App-2 in the n8n credentials.</p>
</li>
<li><p>Update all domain placeholders <a target="_blank" href="http://app1domain.com"><code>app1domain.com</code></a> / <a target="_blank" href="http://app2domain.com"><code>app2domain.com</code></a>.</p>
</li>
<li><p>Adjust the uniqueness key used in the dedupe code (<code>field_name_text</code> in your flow).</p>
</li>
<li><p>Fix typos in node templates (<code>data_tyep_1_id</code> → <code>data_type_1_id</code>).</p>
</li>
<li><p>Set safe <code>batchSize</code> (start small).</p>
</li>
<li><p>Add mapping persistence (recommended).</p>
</li>
<li><p>Run 3–5 sample parents and validate.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to Merge PDFs in Bubble Without Timeouts Using n8n?]]></title><description><![CDATA[If you try merging PDFs directly in Bubble.io, you may run into timeout issues - many users report external API calls taking more than ~30 seconds simply fail.To build a more scalable solution, I moved the heavy lifting to n8n, which doesn’t have Bub...]]></description><link>https://anishgandhi.com/how-to-merge-pdfs-in-bubble-without-timeouts-using-n8n</link><guid isPermaLink="true">https://anishgandhi.com/how-to-merge-pdfs-in-bubble-without-timeouts-using-n8n</guid><category><![CDATA[Bubble timeout]]></category><category><![CDATA[Bubble API]]></category><category><![CDATA[Bubble backend workflow]]></category><category><![CDATA[bubble.io]]></category><category><![CDATA[pdf merge]]></category><category><![CDATA[n8n]]></category><category><![CDATA[APIs]]></category><category><![CDATA[no code automation]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Thu, 09 Oct 2025 14:22:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760018937495/7879126c-9c3b-4c17-93b9-174b326d33a0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you try merging PDFs directly in <strong>Bubble.io</strong>, you may run into <strong>timeout issues</strong> - many users report external API calls taking more than ~30 seconds simply fail.<br />To build a more scalable solution, I moved the heavy lifting to <strong>n8n</strong>, which doesn’t have Bubble’s runtime restrictions. In this article, I walk through the design, implementation, and security considerations of combining Bubble, n8n, and the I Love PDF APIs.</p>
<hr />
<h2 id="heading-bubble-api-workflow-limits">Bubble API Workflow Limits</h2>
<ul>
<li><p>Bubble’s workflows (especially when calling an external API) are subject to <strong>timeout constraints</strong> -many users encounter errors like <code>Failed to establish a connection after 30000 ms</code> or <code>request timed out</code> When the external call takes longer.</p>
</li>
<li><p>Some forum discussion suggests that Bubble has a <strong>5-minute limit</strong> on entire workflow sequences, but the more immediate bottleneck is the <strong>~30s</strong> external API call ceiling.</p>
</li>
<li><p>This means that merging multiple or large PDF files inside a Bubble workflow is inherently risky - you might hit failures when the backend process takes too long or the HTTP request to an API doesn't respond fast enough.</p>
</li>
</ul>
<p>Because of that, I decided to offload the process into a dedicated automation environment (n8n) so that Bubble just orchestrates inputs and gets the merged result back.</p>
<hr />
<h2 id="heading-bubble-pdfs-merge-using-n8n-architecture">Bubble PDFs Merge using n8n Architecture</h2>
<p>Here’s the improved design, refined with caveats:</p>
<h3 id="heading-bubble-n8n-webhook-trigger">Bubble → n8n (Webhook Trigger)</h3>
<ul>
<li><p>Bubble sends the list of PDF URLs + metadata to an <strong>n8n webhook</strong> (HTTP Request).</p>
</li>
<li><p>To avoid Bubble waiting indefinitely, the webhook should <strong>respond quickly</strong> (e.g. acknowledge receipt).</p>
</li>
<li><p>Later, n8n will <strong>call back Bubble</strong> with the merged file link.</p>
</li>
</ul>
<h3 id="heading-n8n-pdf-download-amp-i-love-pdf-merge"><strong>n8n: PDF Download &amp; I Love PDF Merge</strong></h3>
<ul>
<li><p>Use n8n nodes (HTTP Request, Function, Loop) to download each PDF file.</p>
</li>
<li><p>Use <strong>I Love PDF Merge API</strong> (start merge task → upload each file → execute merge).</p>
</li>
<li><p>Store credentials (I Love PDF API key) securely in n8n’s credentials system, not in nodes.</p>
</li>
</ul>
<h3 id="heading-n8n-upload-merged-pdf-to-bubble-file-manager"><strong>n8n: Upload Merged PDF to Bubble File Manager</strong></h3>
<ul>
<li><p>Use Bubble’s <code>/fileupload</code> endpoint to push the merged file.</p>
</li>
<li><p><strong>Security warning</strong>: Bubble’s file upload API endpoint can be abused if left open. Many in the Bubble community call this a vulnerability.</p>
</li>
<li><p>To protect this, ensure only your backend (n8n) can call it (e.g., with a secret header, IP whitelisting, or checking an authorization token).</p>
</li>
</ul>
<h3 id="heading-n8n-bubble-callback-workflow-trigger"><strong>n8n → Bubble: Callback / Workflow Trigger</strong></h3>
<ul>
<li><p>After upload, n8n calls a Bubble API endpoint (e.g. a backend workflow) passing the file URL or file ID.</p>
</li>
<li><p>Bubble updates its database and triggers subsequent workflows (e.g. emailing the PDF).</p>
</li>
</ul>
<h3 id="heading-error-handling-retries"><strong>Error Handling / Retries</strong></h3>
<ul>
<li><p>If any PDF upload or merge step fails, n8n can retry.</p>
</li>
<li><p>You can notify the admin or fallback logic inside n8n if something goes wrong.</p>
</li>
</ul>
<hr />
<h2 id="heading-why-this-bubble-n8n-solution-works-better">Why This Bubble + n8n Solution Works Better?</h2>
<ul>
<li><p><strong>No Bubble timeouts on heavy processing</strong> - n8n can run as long as needed without the same constraints.</p>
</li>
<li><p><strong>Separation of concerns</strong> - Bubble remains the UI and data orchestrator; heavy file processing is handled elsewhere.</p>
</li>
<li><p><strong>Credential safety</strong> - API keys remain in n8n’s credential store, not exposed in visuals.</p>
</li>
<li><p><strong>Better error control</strong> - n8n supports structured retry logic, branching, error-handling, logging etc.</p>
</li>
</ul>
<h3 id="heading-however-you-need-to-be-cautious-about"><strong>However, you need to be cautious about:</strong></h3>
<ul>
<li><p>Ensuring the initial webhook from Bubble doesn’t block (i.e., respond quickly, don’t wait for the full merge).</p>
</li>
<li><p>Securing Bubble’s file upload endpoint so that malicious actors can’t upload arbitrary files.</p>
</li>
<li><p>Handling edge cases (network failures, large file sizes, etc.) robustly.</p>
</li>
</ul>
<hr />
<h2 id="heading-security-amp-best-practices">Security &amp; Best Practices</h2>
<ul>
<li><p>Always use <strong>n8n’s credential store</strong> (not embedding secrets in nodes). n8n’s docs specify credentials are separate and private.</p>
</li>
<li><p>For your Bubble file upload endpoint:</p>
<ul>
<li><p>Avoid leaving it wide open. Many community posts state that the <code>/fileupload</code> endpoint can be triggered without authentication in some setups.</p>
</li>
<li><p>Use headers or tokens to limit who can upload.</p>
</li>
</ul>
</li>
<li><p>For Bubble → n8n webhook:</p>
<ul>
<li><p>Validate incoming requests (signature, token) to prevent unwanted triggers.</p>
</li>
<li><p>Return a fast ACK, then process asynchronously.</p>
</li>
</ul>
</li>
<li><p>Ensure your callback from n8n → Bubble is also authenticated/validated.</p>
</li>
</ul>
<hr />
<h2 id="heading-n8n-template-for-pdf-merger">n8n template for PDF Merger</h2>
<p>Here is the logic structure:</p>
<pre><code class="lang-plaintext">Bubble frontend → HTTP POST to n8n webhook (pdfUrls, metadata)
↓
n8n: Webhook receives → immediately respond 200 OK (maybe return a job ID)
↓
n8n: Process flow
    • Authenticate with I Love PDF (using secure credential)
    • Start merge task
    • For each URL: download and upload to merge task
    • Execute merge → get merged PDF URL
    • Download merged PDF
    • Upload to Bubble fileupload endpoint (with authorization)
↓
n8n → HTTP POST back to Bubble backend workflow endpoint: pass merged file URL &amp; job ID
↓
Bubble backend workflow: update DB, trigger email or next step
</code></pre>
<p>Here is the template to import: <a target="_blank" href="https://drive.google.com/file/d/1sSIMr6V7xVAjsaHJSvUy32jkOJRuXBFz/view?usp=drive_link">Click here to request access.</a></p>
]]></content:encoded></item><item><title><![CDATA[How to Implement Daisy Chain Filtering in Bubble.io?]]></title><description><![CDATA[TL;DR
Daisy Chain Filtering in Bubble.io is a client-side filtering technique where each filter input (like dropdowns or checkboxes) updates a shared custom state. As users select options, each filter refines the results based on the previous ones - ...]]></description><link>https://anishgandhi.com/how-to-implement-daisy-chain-filtering-in-bubbleio</link><guid isPermaLink="true">https://anishgandhi.com/how-to-implement-daisy-chain-filtering-in-bubbleio</guid><category><![CDATA[daisy chain filter]]></category><category><![CDATA[bubble.io]]></category><category><![CDATA[Bubble Developers]]></category><category><![CDATA[filtering]]></category><category><![CDATA[No Code]]></category><category><![CDATA[bubble]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Thu, 07 Aug 2025 09:53:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754560422468/5e948677-cf52-478e-9811-09d30f8ac03a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<h3 id="heading-tldr">TL;DR</h3>
<p>Daisy Chain Filtering in <a target="_blank" href="http://Bubble.io"><strong>Bubble.io</strong></a> is a client-side filtering technique where each filter input (like dropdowns or checkboxes) updates a shared custom state. As users select options, each filter refines the results based on the previous ones - forming a seamless, chained filtering logic.</p>
<p>This method avoids heavy searches or backend workflows and enables fast, dynamic filtering - ideal for datasets under ~1,000 records. You’ll use conditional visibility, custom states, and custom events to build the logic.</p>
</blockquote>
<h2 id="heading-what-is-daisy-chain-filtering">What is Daisy Chain Filtering?</h2>
<p><strong>Daisy chain filtering</strong> is a technique where multiple filters are applied in a specific sequence, one after another, to progressively narrow down a list of results. Each filter takes the output of the previous step and applies an additional condition, creating a “chain” of filters.</p>
<p>In <a target="_blank" href="http://Bubble.io"><strong>Bubble.io</strong></a>, this is typically done using repeated <code>:filtered</code> operations on a list, instead of filtering everything at once or using database constraints.</p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-how-to-implement-daisy-chain-filtering-in-bubbleio">How to implement Daisy Chain Filtering in bubble.io?</h2>
<p>I will guide you through a step-by-step process to implement Daisy Chain Filtering in bubble.io with an example given below.</p>
<h3 id="heading-step-1-create-ui-for-filters">Step 1: Create UI for filters</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754491384123/228a7094-77a2-4d35-bb41-9535303f7a57.png" alt class="image--center mx-auto" /></p>
<p>As you can see in the image above, I am using the following elements in bubble.io for filtering the data of the Repeating Group of products.</p>
<h3 id="heading-step-2-set-custom-states-for-each-filter">Step 2: Set custom states for each filter</h3>
<p>For each filter, I am setting a custom state on the Page.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754492727179/3759bcf5-934a-4102-ac2e-1e804b2f8e04.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-step-3-load-all-data-in-the-popup-hidden-variable-as-base-data">Step 3: Load all data in the Popup Hidden variable as base data</h3>
<p>As you can see in the image, I am loading all the product data on the page</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754492989168/e822d369-7c68-45e3-9e1d-69e2a248538f.png" alt class="image--center mx-auto" /></p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h3 id="heading-step-4-set-the-source-of-the-repeating-group-on-the-page">Step 4: Set the source of the repeating group on the page</h3>
<p>Set the <code>VAR RG ALL Products</code> as the data source of the Repeating Group, where filtered results will be displayed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754493339894/1238497c-97bf-428b-be94-49c668071e93.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-step-5-set-up-6-custom-events-for-5-filters">Step 5: Set up 6 custom events for 5 filters</h3>
<p>As you can see, I have numbered it 00 to 05.</p>
<ul>
<li><p>00 is for setting up base data</p>
</li>
<li><p>01 to 05 for each custom event for each filter</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754493862959/fcc5e301-15fa-488a-bb6c-a89d1d48528a.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-step-6-set-up-a-00-custom-event-to-load-basic-data-in-the-repeating-group">Step 6: Set up a 00 custom event to load basic data in the Repeating Group</h3>
<p>As you can see in the image, first, I am displaying base data in the Repeating group and triggering the 01 custom event</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754494355213/85f5c821-81e1-4aab-b122-9e5f43627f8f.png" alt class="image--center mx-auto" /></p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h3 id="heading-step-7-set-up-a-01-to-05-custom-event-to-filter-the-repeating-group-data-based-on-active-filters">Step 7: Set up a 01 to 05 custom event to filter the Repeating Group data based on active filters</h3>
<p>As you can see in the 00 Filter workflow, the 01 workflow is triggered in the end. Repeat this process for workflows 01 to 04.<br />So,</p>
<ul>
<li><p>Custom ‘00 Filter’ workflow will trigger ‘01 Filter’ workflow</p>
</li>
<li><p>Custom ‘01 Filter’ workflow will trigger ‘02 Filter’ workflow</p>
</li>
<li><p>Custom ‘02 Filter’ workflow will trigger ‘03 Filter’ workflow</p>
</li>
<li><p>Custom ‘03 Filter’ workflow will trigger ‘04 Filter’ workflow</p>
</li>
<li><p>Custom ‘04 Filter’ workflow will trigger ‘05 Filter’ workflow</p>
</li>
</ul>
<p>Now, as you can see in the image attached in step 5, I have assigned each custom event from 01 to 05 for each filter.</p>
<ul>
<li><p>Custom ‘00 Filter’ workflow is base data loading custom event</p>
</li>
<li><p>Custom ‘01 Filter’ workflow is an Input filter custom event</p>
</li>
<li><p>Custom ‘02 Filter’ workflow is a price range filter custom event</p>
</li>
<li><p>Custom ‘03 Filter’ workflow is a brand filter custom event</p>
</li>
<li><p>Custom ‘04 Filter’ workflow is a category filter custom event</p>
</li>
<li><p>Custom ‘05 Filter’ workflow is a location filter custom event</p>
</li>
</ul>
<p>On 01 to 05 custom events, before triggering the next event, use the action ‘Display data in Repeating Group’ with the condition that if the filter value is not empty, then run this action. Just as shown in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754552897495/d120a304-7996-45d8-b475-2059f58685f1.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-step-8-trigger-00-custom-event-after-every-action-of-setting-the-custom-state-for-filters">Step 8: Trigger 00 Custom event after every action of setting the custom state for filters</h3>
<p>As you can see in the image below, I am adding a category in custom state when the user selects a category from the category dropdown. In the next action, I am triggering the 00 Custom event</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754553697786/b00c8de1-6e1f-49df-8664-04c921256c35.png" alt class="image--center mx-auto" /></p>
<p><strong>Similarly, Trigger 00 custom event after all actions, whenever the value of the custom state for any filter changes.</strong></p>
<p>And here your daisy chain filter is ready. Now, let’s understand how this will work</p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-how-this-daisy-chain-filter-will-be-executed">How this Daisy chain filter will be executed?</h2>
<p>Let’s understand this in a step-by-step flow chart manner:</p>
<ol>
<li><p>User selects a filter (e.g., Brand = Nike)</p>
</li>
<li><p>Custom state is updated (brand = "Nike")</p>
</li>
<li><p>Trigger "00 Filter" → loads all products</p>
</li>
<li><p>Triggers "01 Filter" (Input) → skips if empty</p>
</li>
<li><p>Triggers "02 Filter" (Price) → skips if not set</p>
</li>
<li><p>Triggers "03 Filter" (Brand) → filters by brand = "Nike"</p>
</li>
<li><p>Triggers "04 Filter" (Category) → skips if not selected</p>
</li>
<li><p>Triggers "05 Filter" (Location) → skips if not selected</p>
</li>
<li><p>Final result shown in the Repeating Group Products</p>
</li>
</ol>
<h2 id="heading-frequently-asked-questions">❓ Frequently Asked Questions</h2>
<h3 id="heading-q1-your-daisy-chain-method-feels-very-fast-once-the-data-is-loaded-but-what-happens-when-my-database-grows-from-200-products-to-over-10000-is-this-client-side-method-still-the-best-choice">Q1: Your "Daisy Chain" method feels very fast once the data is loaded. But what happens when my database grows from 200 products to over 10,000? Is this client-side method still the best choice?</h3>
<p>This is a crucial question for any growing application. The Daisy Chain method is exceptionally fast for user interactions <em>after</em> the initial data is loaded. However, its scalability is limited.</p>
<ul>
<li><p><strong>For Small Datasets (&lt; 500 items):</strong> This method is excellent. The initial load time in <code>Step 3</code> is negligible, and the instant filtering provides a fantastic user experience.</p>
</li>
<li><p><strong>For Large Datasets (&gt; 1,000 items):</strong> The initial step of loading <em>all</em> items into a custom state becomes a major performance bottleneck. A user might have to wait a long time for 10,000+ items to download to their browser, and on lower-powered devices, this could even crash the browser page.</p>
</li>
</ul>
<p><strong>My Recommendation:</strong></p>
<ul>
<li><p>For large datasets, you must switch to <strong>server-side filtering</strong>. This means your Repeating Group's data source should be <code>Do a search for Products</code>, with the constraints dynamically referencing your filter inputs (e.g., <code>Brand = Dropdown Brand's value</code>). Use the "Ignore empty constraints" feature to build a single, powerful search query that only downloads the exact data needed from the server.</p>
</li>
<li><p>When your filtering logic is too complex for standard server-side constraints, you should use the hybrid approach. Here is an example:</p>
<ul>
<li><p><strong>Filtering Based on a Calculation:</strong> This is the most common use case. You want to filter based on a value that is calculated from two or more fields on the same data entry.</p>
<ul>
<li><p><strong>Example:</strong> You have a <code>Product</code> with <code>regular_price</code> and a <code>sale_price</code>. You want to show only products that are "more than 20% off." The required filter logic is <code>sale_price &lt; regular_price * 0.8</code>. You <strong>cannot</strong> perform this calculation within a <code>Do a search for...</code> constraint.</p>
</li>
<li><p><strong>Hybrid Solution:</strong> First, do a server-side search for all products in the relevant category (e.g., <code>Category = "Shoes"</code>). Then, apply a client-side <code>:filtered</code> with a <code>Advanced:</code> condition: <code>This Product's sale_price &lt; This Product's regular_price * 0.8</code>.</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p>Here is the golden rule for filtering in Bubble for <strong>Large Datasets</strong>:</p>
<blockquote>
<p><strong>Filter as much as possible, as early as possible, on the server.</strong></p>
</blockquote>
<p>  Use the standard constraints in your <code>Do a search for...</code> to do all the heavy lifting. Get the dataset as small as you possibly can <em>before</em> it ever leaves the database.</p>
<p>  Only use the hybrid approach as a <strong>final step</strong> to perform a specific calculation or logical check that is impossible to do on the server. It is a powerful tool for complex situations, but relying on it for simple filters is an anti-pattern that will lead to performance issues as your app scales.</p>
</li>
</ul>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h3 id="heading-q2-this-article-mentions-saving-workload-units-wu-by-avoiding-multiple-do-a-search-for-calls-how-does-the-wu-cost-of-this-method-compare-to-a-single-optimized-server-side-search">Q2: This article mentions saving Workload Units (WU) by avoiding multiple "Do a search for" calls. How does the WU cost of this method compare to a single, optimized server-side search?</h3>
<p>You're right that this method cleverly avoids the high WU cost of running <em>multiple separate searches</em> and merging them. However, it's a trade-off. Let's compare:</p>
<ol>
<li><p><strong>Daisy Chain Method:</strong> The main WU cost is in <code>Step 3: Load all data</code>. Loading a table with X number of items, even with minimal fields, can consume a significant amount of WU in a single burst. After that, all the <code>:filtered</code> Operations are free as they happen on the client.</p>
</li>
<li><p><strong>Server-Side Method:</strong> A <code>Do a search for...</code> with several constraints is extremely efficient. The WU cost is proportional to the <em>final number of results returned</em>, not the total size of the database table.</p>
</li>
</ol>
<p><strong>My recommendation:</strong></p>
<ul>
<li><p>For <strong>small to medium Datasets</strong>, this method is likely more WU-efficient.</p>
</li>
<li><p>For <strong>large tables</strong>, a single, well-constrained server-side search is almost always <strong>more WU-efficient and infinitely more performant</strong> because the cost is based on the small, filtered result set, not the entire database table.</p>
</li>
</ul>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h3 id="heading-q3-are-there-any-security-implications-of-loading-all-product-data-into-a-custom-state-on-page-load-as-shown-in-step-3">Q3: Are there any security implications of loading all product data into a custom state on page load, as shown in Step 3?</h3>
<p><strong>Yes, and this is the most critical consideration.</strong> Any data loaded onto the page - even if held in a hidden variable or custom state - is fully accessible to a savvy user via their browser's developer tools.</p>
<p>Imagine your <code>Product</code> data type also has fields like <code>cost_price</code> or <code>internal_notes</code>. Even if you don't display these fields in the repeating group, by loading the "full list of Products" to the client, you are sending that sensitive data to every user.</p>
<p><strong>Rule of Thumb:</strong> Never use client-side filtering for data that isn't 100% public. The only way to truly secure data is to use <strong>Bubble's Privacy Rules</strong> to define what data a user is allowed to see and <strong>server-side search constraints</strong> to ensure only that permitted data is ever sent from the database.</p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h3 id="heading-q4-when-does-daisy-chain-filtering-make-sense-in-bubbleio">Q4: When Does Daisy Chain Filtering Make Sense in Bubble.io?</h3>
<p>I want to get ahead of a common (and valid) reaction:</p>
<p><strong>“Why not just use a single</strong> <code>Do a search for</code> with <code>ignore empty constraints</code>?”<br />So here’s a breakdown of when I think <strong>Daisy Chain Filtering (DCF)</strong> is a useful tool and when it’s not.</p>
<h3 id="heading-when-not-to-use-daisy-chain-filtering">❌ When <em>Not</em> to Use Daisy Chain Filtering</h3>
<p>Daisy chaining is a <strong>client-side pattern</strong>, which means:</p>
<ul>
<li><p><strong>All data is loaded into the browser first</strong> (<code>:filtered</code> is evaluated client-side)</p>
</li>
<li><p><strong>Filters apply after page load</strong>, meaning initial data transfer can be large</p>
</li>
<li><p>It exposes <strong>all fields (not protected by privacy rules) of all loaded records</strong>, which could risk <strong>data leaks</strong></p>
</li>
<li><p>It <strong>doesn’t scale</strong> well for large datasets (&gt;1,000 records, depending on complexity)</p>
</li>
<li><p>It increases <strong>initial memory usage</strong> and may <strong>freeze or crash</strong> low-resource devices</p>
</li>
</ul>
<p>So if you're working with:</p>
<ul>
<li><p>Dynamic but <strong>searchable constraints</strong> (e.g. text contains, category is, status is not)</p>
</li>
<li><p>Data that can safely be retrieved directly from the server</p>
</li>
<li><p>A need for <strong>server-side filtering or pagination</strong></p>
</li>
</ul>
<p>👉 Then, yes - <strong>a standard</strong> <code>Do a search for</code> with "Ignore empty constraints" is the best and most scalable choice. No argument there.</p>
<h3 id="heading-when-daisy-chain-filtering-can-be-the-better-option">✅ When Daisy Chain Filtering <em>Can Be</em> the Better Option</h3>
<p>That said, DCF isn’t a mistake - it’s a <strong>targeted tradeoff</strong>. It works best when:</p>
<ul>
<li><p>You have a <strong>bounded dataset (~&lt;1,000 records)</strong> that is <strong>safe</strong> to load into the browser</p>
</li>
<li><p>You want <strong>ultra-fast interactions</strong>, especially when filters are toggled in rapid succession</p>
</li>
<li><p>You need <strong>complex filter logic</strong> that Bubble can’t express server-side<br />  e.g.,</p>
<ul>
<li><p>Filters that involve <strong>computed fields</strong> (like discounts or margin %)</p>
</li>
<li><p><strong>Nested field matching,</strong> where native Bubble search is limited</p>
</li>
<li><p><strong>Dynamic comparison logic</strong> (e.g., user-defined operators like “merged with”, “contains”)</p>
</li>
</ul>
</li>
<li><p>You're building a <strong>“multi-step” filter UX</strong> with visible stages, where each filter step refines the next (hence the chain)</p>
</li>
</ul>
<p>These are areas where <strong>chaining filters through custom states and conditional events</strong> gives you granular control and better perceived performance, as long as you’re aware of the memory and security costs.</p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-final-thought">Final Thought</h2>
<p>I’m not claiming DCF is <em>better</em> than server-side filtering - I’m saying it’s different.<br />Used intentionally, <strong>it's a specialized tool</strong>, not a default pattern.</p>
<p>And the article I wrote here breaks down <strong>where the tipping point lies</strong> - when DCF becomes faster, and when it becomes dangerous.</p>
]]></content:encoded></item><item><title><![CDATA[How to Let Admins Access User Accounts in Bubble.io Without Credentials]]></title><description><![CDATA[TL;DR:
Let your admins log in as any user in Bubble.io — no passwords, no backend workflows.
Just:

Generate a magic login link (without emailing it),

Grab the link using a Toolbox Server Script, and

Instantly redirect the admin to log in as that u...]]></description><link>https://anishgandhi.com/how-to-let-admins-access-user-accounts-in-bubbleio-without-credentials</link><guid isPermaLink="true">https://anishgandhi.com/how-to-let-admins-access-user-accounts-in-bubbleio-without-credentials</guid><category><![CDATA[bubbletips]]></category><category><![CDATA[bubbletutorial]]></category><category><![CDATA[loginasuser]]></category><category><![CDATA[bubble.io]]></category><category><![CDATA[No Code]]></category><category><![CDATA[bubble]]></category><category><![CDATA[Impersonation]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Mon, 04 Aug 2025 12:46:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754236761991/78e2099d-99a9-4def-a620-77ce53871173.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<h3 id="heading-tldr">TL;DR:</h3>
<p>Let your admins <strong>log in as any user</strong> in <a target="_blank" href="http://Bubble.io">Bubble.io</a> — no passwords, no backend workflows.</p>
<p>Just:</p>
<ol>
<li><p>Generate a <strong>magic login link</strong> (without emailing it),</p>
</li>
<li><p>Grab the link using a <strong>Toolbox Server Script</strong>, and</p>
</li>
<li><p>Instantly <strong>redirect the admin</strong> to log in as that user.</p>
</li>
</ol>
<p>Perfect for support, QA, and testing — all in 3 simple steps.</p>
</blockquote>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-use-case">🧐 Use Case</h2>
<p>Let’s say your APP admin wants to see what a specific user sees in their account. Instead of asking for login credentials, you provide a <strong>“Run as”</strong> button next to every user in your Admin panel.</p>
<p>With one click, your app logs the admin in as that user and redirects them to the user’s dashboard.</p>
<h2 id="heading-requirements">✅ Requirements</h2>
<ul>
<li><p>Bubble app with user roles (admin/user)</p>
</li>
<li><p>Toolbox Plugin</p>
</li>
<li><p>Admin Panel, which only the Admin can access</p>
</li>
</ul>
<h2 id="heading-step-by-step-implement-run-as-user-in-bubble">🚀 Step-by-Step: Implement “Run as User” in Bubble</h2>
<h3 id="heading-step-1-add-run-as-button-in-admin-panel-user-list">✅ Step 1: Add “Run as” Button in Admin Panel User list</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754217027349/2102e8a4-4cf8-4910-935c-b0dba2a7b5f8.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-step-2-set-up-the-magic-link-workflow-on-button-click">✅ Step 2: Set up the Magic link workflow on button click</h3>
<p>Make sure to just create a token and not send an email. Here is what it will look like</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754217279815/e2875d4f-4e4e-4b3c-aeff-6083b8a0f8d6.png" alt class="image--center mx-auto" /></p>
<p>⚠️ This sends a login link to the backend. It won’t send an actual email and you're just using the generated login link internally.</p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h3 id="heading-step-3-use-server-script-toolbox">✅ Step 3: Use Server Script (Toolbox)</h3>
<p>Add a <strong>"Server Script"</strong> action from the Toolbox plugin right after the magic link action.</p>
<p>In the script box, extract the login link like this:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> link = <span class="hljs-string">"Result of step 1 (Send magic login link)"</span>;
link;
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754217476198/01e19b45-c1a0-4642-9d11-5d1c7c2a5c97.png" alt class="image--center mx-auto" /></p>
<p>Make sure to set the return type as text.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754314005322/546a0786-8727-4bd8-8271-e6d9a1e3e83a.png" alt class="image--center mx-auto" /></p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h3 id="heading-step-4-redirect-using-open-external-website">✅ Step 4: Redirect Using “Open External Website”</h3>
<p>Add a final workflow step:</p>
<ul>
<li><p>Action: <strong>Open an external website</strong></p>
</li>
<li><p>URL: <code>Result of Step 2 (Server Script)</code></p>
</li>
</ul>
<p>This step instantly redirects the admin to the magic login link, logging them in as the selected user!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754217771526/1269c143-7076-43e5-b0a9-2f2354cc9509.png" alt class="image--center mx-auto" /></p>
<p>With this, the admin user will be logged out, and the admin will be signed in as a user to whose email was used in to create the magic link.</p>
<h2 id="heading-security-best-practices">🔐 Security Best Practices</h2>
<ul>
<li><p>✅ Only show the “Run as” button to users with the <strong>Admin role</strong>.</p>
</li>
<li><p>✅ Add privacy rules to restrict access.</p>
</li>
<li><p>✅ Log impersonation actions (optional but good for audit trail).</p>
</li>
</ul>
<h2 id="heading-final-outcome">🧩 Final Outcome</h2>
<p>Your admin can now:</p>
<ul>
<li><p>Click “Run as” on any user</p>
</li>
<li><p>Instantly be redirected to the app, logged in as that user</p>
</li>
<li><p>Provide support, test behavior, or troubleshoot without credentials</p>
</li>
</ul>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h3 id="heading-faq">❓ FAQ</h3>
<h4 id="heading-what-is-the-run-as-user-feature-in-bubbleiohttpbubbleio"><strong>What is the “Run as User” feature in</strong> <a target="_blank" href="http://Bubble.io"><strong>Bubble.io</strong></a><strong>?</strong></h4>
<p>It’s a functionality that allows admins to log in as any user in the app without knowing their credentials. It’s useful for support, debugging, and user experience testing.</p>
<h4 id="heading-is-it-secure-to-log-in-as-a-user-without-a-password"><strong>Is it secure to log in as a user without a password?</strong></h4>
<p>Yes, if implemented properly. The magic login link is time-bound and only accessible to admins. Ensure your workflows are protected with role-based privacy rules.</p>
<h4 id="heading-does-the-user-receive-an-email-when-i-use-send-magic-login-link"><strong>Does the user receive an email when I use “Send magic login link”?</strong></h4>
<p>No. While Bubble's action generates a login link, it only sends an email if explicitly configured. In this case, the link is used internally and not emailed.</p>
<h4 id="heading-do-i-need-backend-workflows-to-implement-this"><strong>Do I need backend workflows to implement this?</strong></h4>
<p>No. This method uses only front-end workflows and the Toolbox plugin, making it easier and faster to set up.</p>
<h4 id="heading-can-i-choose-which-page-the-admin-lands-on-after-impersonation"><strong>Can I choose which page the admin lands on after impersonation?</strong></h4>
<p>Yes. The “Send magic login link” action allows you to define a redirect URL. You can set this to any page (like dashboard, settings, etc.).</p>
<h4 id="heading-will-the-actual-user-be-logged-out-when-i-log-in-as-them"><strong>Will the actual user be logged out when I log in as them?</strong></h4>
<p>No. This creates a new session for the admin. The original user remains unaffected and logged in on their own device.</p>
<h4 id="heading-how-long-is-the-login-token-valid"><strong>How long is the login token valid?</strong></h4>
<p>You can set the token expiration manually in the “Send magic login link” action, usually anywhere from 1 minute to 24 hours.</p>
]]></content:encoded></item><item><title><![CDATA[Self-Host n8n in Minutes - No Terminal Commands, No Code (Step-by-Step)]]></title><description><![CDATA[Good news: It’s 2025. You don’t have to be a DevOps wizard to self-host n8n.

🔹 TL;DR: You can self-host n8n on Hostinger in just a few clicks - no terminal, no Docker, and no code. This beginner-friendly guide walks you through setting up a VPS, in...]]></description><link>https://anishgandhi.com/self-host-n8n-in-minutes-no-terminal-commands-no-code-step-by-step</link><guid isPermaLink="true">https://anishgandhi.com/self-host-n8n-in-minutes-no-terminal-commands-no-code-step-by-step</guid><category><![CDATA[automation]]></category><category><![CDATA[self-hosted]]></category><category><![CDATA[n8n]]></category><category><![CDATA[n8n, openai, pinecone, automation, nocode]]></category><category><![CDATA[n8n workflows]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Sun, 03 Aug 2025 08:52:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754211790357/00f48819-56aa-4ce9-ace0-b54d9fda9724.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Good news: It’s 2025. You don’t have to be a DevOps wizard to self-host n8n.</p>
<blockquote>
<p>🔹 <strong>TL;DR</strong>: You can self-host n8n on Hostinger in just a few clicks - no terminal, no Docker, and no code. This beginner-friendly guide walks you through setting up a VPS, installing n8n, and activating a free license. Perfect for non-techies, automation geeks, and anyone tired of cloud pricing.</p>
</blockquote>
<p>In this guide, I’ll show you how to <strong>self-host n8n on Hostinger</strong> in just a few minutes. No terminal. No SSH. No Docker CLI. No YAML. Just simple steps - perfect for non-techies, automation geeks, or anyone tired of Cloud pricing.</p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-but-isnt-self-hosting-hard">🛑 But Isn’t Self-Hosting Hard?</h2>
<p>Normally, yes, for people who don’t know how to set up a server. Most tutorials tell you to:</p>
<ul>
<li><p>Spin up a VPS</p>
</li>
<li><p>Install Docker via the terminal</p>
</li>
<li><p>Run <code>docker-compose</code></p>
</li>
<li><p>Configure environment variables manually</p>
</li>
<li><p>Pray nothing breaks</p>
</li>
</ul>
<p>But if you're using <strong>Hostinger</strong>, there’s a shortcut. You can <strong>deploy n8n using Hostinger's App Installer</strong> and a few clicks.</p>
<p>Let me walk you through it.</p>
<h2 id="heading-step-1-log-in-to-your-hostinger-account">Step 1: Log in to your Hostinger account</h2>
<p>Go to <a target="_blank" href="https://auth.hostinger.com/in/login">https://auth.hostinger.com/in/login</a> and provide your information.</p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-step-2-set-up-vps">Step 2: Set up VPS</h2>
<p>Go to the VPS section and choose KVM VPS</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754207497285/cb02ce7c-1bcb-4fb8-a714-14dafe5d3bf8.png" alt="Hostinger KVM VPS Setup" class="image--center mx-auto" /></p>
<p>Select server location: For me, it’s INDIA. You can choose your nearest server location</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754207602094/f594dfc3-5ce5-4488-b798-e060c8b94c33.png" alt="Select server location on Hostinger" class="image--center mx-auto" /></p>
<p>Now search for n8n in the operating system</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754207786059/3105f768-4e4b-4559-bffa-8022ccac6eb8.png" alt="Search n8n in hostinger" class="image--center mx-auto" /></p>
<p>Now, set up the root password</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754208344727/1a94c5d4-c2d1-4bec-b8ac-4a2a0c48f55a.png" alt="Setup root password for n8n hosting server" class="image--center mx-auto" /></p>
<p>Now choose a plan. If Hostinger is providing price parity for your location, opt for it. I think KVM 2 is good enough.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754208519272/9aecb494-ea59-4a3d-a9a7-b3e1170b23b5.png" alt="Select a hosting plan for n8n" class="image--center mx-auto" /></p>
<p>Once you select the plan and complete the payment, the n8n hosting will be initiated automatically.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754208781952/86163eae-1b0e-4f67-9492-d75e1d4787bd.png" alt="n8n auto hosting via hostinger" class="image--center mx-auto" /></p>
<p>Once the setup is completed, you can go to manage the VPS</p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-step-3-manage-vps">Step 3: Manage VPS</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754209675322/81508c1d-8fdc-46ca-994e-48f7eaab6b47.png" alt="Screenshot of the Hostinger setup page displaying options for managing a VPS. It includes buttons for accessing the VPS dashboard and SSH access, with an arrow pointing to the &quot;Manage VPS&quot; button. Text reads &quot;Well Done, You Are Ready&quot; at the top." class="image--center mx-auto" /></p>
<p>On Dashboard → VPS → Click on Manage app</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754209043189/76645e10-b836-4724-b322-dbc006cdec26.png" alt="Interface displaying n8n built on Ubuntu 24.04, with a button labeled &quot;Manage App&quot; and a link to &quot;Learn more.&quot;" class="image--center mx-auto" /></p>
<p>Now you can set up an owner account</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754209190185/761dfbc7-aea0-45ae-ae24-1362548194e7.png" alt="Alt text: &quot;Account setup form for n8n, requiring email, first name, last name, and password with specific security criteria. Includes an option to receive security and product updates.&quot;" class="image--center mx-auto" /></p>
<p>After setting up the owner account, Hostinger is currently providing some lifetime free features. Just enter your email ID and receive the license key within minutes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754209481645/314d886c-c738-4fa3-a17b-1cac66883196.png" alt="Screenshot of a prompt offering a free activation key for advanced features like workflow history, advanced debugging, and folder organization. Users need to enter their email to receive a lifetime access license key. Options to skip or send the license key are available." class="image--center mx-auto" /></p>
<p>Now go to usage and plan to activate the license using the license key received in your email.</p>
<p>After this, you can sign up with your owner credentials and start using n8n on your self-hosted server.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754210284030/ec9be11d-0222-4a8d-81e1-90728c5b67b8.png" alt="Sign-in form for n8n with fields for email and password, a &quot;Sign in&quot; button, and a &quot;Forgot my password&quot; link." class="image--center mx-auto" /></p>
<p>You don’t need to be a developer to self-host n8n anymore. Tools like <strong>Hostinger</strong> have made it a click-and-go easy.</p>
<p>Whether you’re automating lead generation, connecting Airtable and Gmail, or syncing your Notion database, n8n can handle it.</p>
<p>And now you can host it yourself - <strong>no terminal required</strong>.</p>
<p>To support more articles, buy me a coffee</p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-faqs">❓ FAQs</h2>
<h3 id="heading-can-i-self-host-n8n-without-docker-or-terminal-access">Can I self-host n8n without Docker or terminal access?</h3>
<p>Yes! Hostinger’s App Installer allows you to set up n8n on a VPS without using Docker, SSH, or any command-line tools.</p>
<h3 id="heading-is-there-a-free-version-of-n8n-when-self-hosting">Is there a free version of n8n when self-hosting?</h3>
<p>Yes. Hostinger currently offers a lifetime license key for some advanced n8n features like workflow history, folder organization, and more - just enter your email during setup to claim it.</p>
<h3 id="heading-who-should-use-this-self-hosting-method">Who should use this self-hosting method?</h3>
<p>This guide is ideal for non-techies, no-code builders, indie hackers, and automation enthusiasts who want to control their infrastructure without touching the terminal.</p>
<h3 id="heading-how-long-does-it-take-to-set-up">How long does it take to set up?</h3>
<p>On average, it takes about 10–15 minutes to fully deploy and configure n8n using Hostinger.</p>
<h3 id="heading-is-hostinger-the-only-way-to-self-host-n8n">Is Hostinger the only way to self-host n8n?</h3>
<p>No, there are other ways (e.g., DigitalOcean, Railway, Docker CLI), but Hostinger offers one of the easiest no-code paths for beginners.</p>
]]></content:encoded></item><item><title><![CDATA[How to Paginate Repeating Groups in Bubble.io Without Loading All Data at Once?]]></title><description><![CDATA[Excessive WUs consumption issue in most pagination setup of the Repeating Group in bubble.io
A common mistake developers make is thinking they’ve implemented pagination just because data is displayed page by page. But behind the scenes, Bubble still ...]]></description><link>https://anishgandhi.com/how-to-paginate-repeating-groups-in-bubbleio-without-loading-all-data-at-once</link><guid isPermaLink="true">https://anishgandhi.com/how-to-paginate-repeating-groups-in-bubbleio-without-loading-all-data-at-once</guid><category><![CDATA[Bubble.io pagination]]></category><category><![CDATA[Bubble.io repeating group performance]]></category><category><![CDATA[Load data in chunks Bubble.io]]></category><category><![CDATA[Bubble.io workload units optimization]]></category><category><![CDATA[Bubble.io lazy loading]]></category><category><![CDATA[bubble.io]]></category><category><![CDATA[bubble]]></category><category><![CDATA[Performance Optimization]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Mon, 19 May 2025 11:18:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747652791787/c91de22f-f357-4af1-b88d-95e2a3a232cc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-excessive-wus-consumption-issue-in-most-pagination-setup-of-the-repeating-group-in-bubbleio">Excessive WUs consumption issue in most pagination setup of the Repeating Group in bubble.io</h2>
<p>A common mistake developers make is thinking they’ve implemented pagination just because data is displayed page by page. But behind the scenes, Bubble still loads the <strong>entire dataset</strong> into the browser on page load. Suppose there are 1,000 records and your app shows 10 records per page. If a user only browses the first 3 pages, you still pay the WU cost for fetching <strong>all 1,000 records</strong>, not just the 30 that were actually viewed.</p>
<p>If your Bubble app feels slow or is using more workload units (WUs) than expected, the problem might be how you're handling data in repeating groups. Loading an entire list of items at once can slow down your app and increase your resource usage, especially when dealing with a large database.</p>
<p>In this article, you will learn how to paginate repeating groups in Bubble.io to load data in smaller chunks. This approach improves performance, reduces workload unit consumption, and creates a better experience for your users. Whether you're working on an MVP or scaling an existing application, these techniques will help you build smarter and more efficient apps.</p>
<p>Now I am considering that repeating group is ready and now you are implementing pagination. Here is the best practice to do that.</p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-step-1-setup-hidden-variables">Step 1: Setup Hidden Variables</h2>
<p>Create a popup named Popup Hidden variables and create following Groups shown in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747643879995/b4c42993-0b4a-4a55-a8e3-96eed9394317.png" alt="Setup variables for Pagination in bubble.io repeating group" class="image--center mx-auto" /></p>
<p>Set all these group’s <strong>type of content</strong> as <strong>number.</strong></p>
<p>Here is the data source for each group:</p>
<ul>
<li><p>Group Item_from - 1</p>
</li>
<li><p>Group Total_count - Do a search for:count [Do a search for:Count will not load all data. If you inspect, you will find out in <code>network tab → maggregate</code> that this will return number only from server]</p>
</li>
<li><p>Group Current_page - 1</p>
</li>
<li><p>Group Item_until - 10</p>
</li>
<li><p>Group Show_per_page - 10</p>
</li>
</ul>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-step-2-setup-ui-for-pagination">Step 2: Setup UI for Pagination</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747644833847/37454aae-8f57-456f-a35d-0199c4c132da.png" alt="Pagination UI setup bubble.io editor view" class="image--center mx-auto" /></p>
<p>After, The Show per page: is a dropdown which will static values like shown in the image below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747644976122/7868c51f-10ba-4604-8817-b2e4366fed30.png" alt="Bubble.io pagination : Select number of entries per page" class="image--center mx-auto" /></p>
<p>Add Toolbox plugin (It’s free!) <a target="_blank" href="https://bubble.io/plugin/toolbox-1488796042609x768734193128308700">Here is the link.</a></p>
<p>Add List of Numbers element of toolbox plugin on the page and configure it as shown in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747645770919/3ba6ca86-e06f-4b13-9538-2f603ef37af0.png" alt="Bubble.io pagination - Generate dynamic data of number of pages" class="image--center mx-auto" /></p>
<p>Between Prev and Next button there will be Repeating Group of page numbers which has List of numbers as data source shown below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747646347322/0a35c5f8-85b9-4278-bc31-f87122f3b3e1.png" alt="Bubble.io pagination: Repeating Group page numbers data source" class="image--center mx-auto" /></p>
<p>Setup View 1-50 of 2,500 results as per the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747646744606/69f0783e-8dae-454d-98a1-86688db96ac6.png" alt="Bubble.io pagination - Show pagination result" class="image--center mx-auto" /></p>
<p>With this, UI setup of Pagination Group is completed.</p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-step-3-setup-repeating-group-source">Step 3: Setup Repeating Group Source</h2>
<p>Here I am loading Email templates in Repeating Group, so the data source setup will be as per shown in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747647247835/3909243e-727e-4c14-94a8-f220c0647487.png" alt="Bubble.io pagination : Repeating Group data source" class="image--center mx-auto" /></p>
<p>Remember, Use :items until# before using :items from#. Here is the logic behind it given by bubble.io</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747647499702/972e3414-e8fc-4a8e-b874-706f6743646b.png" alt="Bubble.io pagination: Sequence of constrains" class="image--center mx-auto" /></p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-step-4-setup-pagination-workflows">Step 4: Setup Pagination Workflows</h2>
<p>I don’t recommend using display data in repeating group action as in my experience, It is not good practice for performance! But it is ok to use display data in group as the number of item will always be one!</p>
<h3 id="heading-create-2-custom-events">Create 2 custom events:</h3>
<ol>
<li><p>update-page:</p>
<p> This event will update page number in Group Current_page as shown in the image below</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747648839081/4988f3ba-b74f-4583-b0a3-73fa51fc7cfb.png" alt="Bubble.io pagination: Custom event for setting page and entry numbes" class="image--center mx-auto" /></p>
</li>
<li><p>update-list:</p>
<p> This event will update from and until number in Group Item_from and Group Item_until as shown in the image below</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747649574060/784c2d77-3d3b-4d9c-a9c0-552096f20f12.png" alt="Bubble.io pagination: Update Entries of Variables" class="image--center mx-auto" /></p>
<p> Here is the <strong>formula for data update of Group Item_from</strong>: <code>[{( Current page number * Show per page number ) - Show per page number} + 1]</code></p>
<p> Here is the <strong>formula for data update of Group Item_until</strong>: <code>Current page number * Show per page number</code></p>
</li>
</ol>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h3 id="heading-create-dropdown-show-per-page-value-change-workflow-event">Create Dropdown Show per page value change workflow event:</h3>
<p>On the UI of pagination, there is the dropdown from where user can select how many entries per page user wants to see like 10 or 30 entries per page. User can change the value from the dropdown as we have seen in Step 2. Now when user changes the value of dropdown, this workflow will trigger as per shown in the image below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747650622358/7ad440f3-29ba-49b2-b3f0-ca0a4045b097.png" alt="Create Dropdown Show per page value change workflow event" class="image--center mx-auto" /></p>
<p>So here there are 3 actions:</p>
<ul>
<li><p>Display data in Group Show_per_page</p>
</li>
<li><p>Trigger update-page</p>
</li>
<li><p>Trigger update list</p>
</li>
</ul>
<h3 id="heading-create-page-number-click-workflow-event">Create Page number click workflow event:</h3>
<p>Page numbers are part of Repeating group List of numbers’ numbers so on click of this Repeating Group’s group, this workflow will trigger as per image shown below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747651558051/345abc3d-1423-46d7-827d-02c83ef9c4ad.png" alt="Bubble.io pagination: Create Page number click workflow event" class="image--center mx-auto" /></p>
<p>This will set current page as whatever number user has clicked</p>
<h3 id="heading-create-previous-and-next-button-click-workflow-event">Create Previous and Next button click workflow event:</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747651806202/d049684c-b3b9-4856-854a-50f8d5a20b01.png" alt="Bubble.io pagination - Create Previous and Next button click workflow event" class="image--center mx-auto" /></p>
<p>And here your optimised performant Pagination Ready:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747651918407/642e9b49-7776-4d8a-8801-ce54cb47fc7b.png" alt="Load data in chunks Bubble.iou" class="image--center mx-auto" /></p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h3 id="heading-ux-suggestion">UX Suggestion:</h3>
<p>To improve UX when User is clicking and Repeating Group is loading, you can add skeleton loading for better UX. <a target="_blank" href="https://anishgandhi.com/how-to-create-skeleton-loading-in-bubble">Here is the article link about How to create Skeleton Loading in Bubble?</a></p>
<h2 id="heading-conclusion">🔚 Conclusion</h2>
<p>Pagination in Bubble.io isn’t just about showing fewer records per page — it’s about <strong>how</strong> the data is fetched and rendered. Most developers unknowingly fall into the trap of client-side pagination, where the entire dataset is loaded up front, leading to slow performance and unnecessary workload unit (WU) consumption.</p>
<p>By implementing true server-side pagination, as outlined in this article, you can dramatically reduce WUs, improve load times, and scale your app more efficiently. Using hidden variables, structured pagination logic, and smart workflows, your repeating groups will load only the data that’s actually needed — no more, no less.</p>
<p>If you care about performance, cost optimization, and creating a seamless user experience, building pagination the right way is a must. Apply this setup to your current and future projects, and you’ll see the difference in both speed and server costs.</p>
]]></content:encoded></item><item><title><![CDATA[How to Use Sub-Apps in Bubble? - The Complete Step-by-Step Guide]]></title><description><![CDATA[What is a sub-app in bubble.io?
A sub-app is a separate app instance created from your main Bubble app. It:

It has its own database (you can optionally copy data from the main app at creation).

Has its own domain, users, privacy rules, and media up...]]></description><link>https://anishgandhi.com/how-to-use-sub-apps-in-bubble-the-complete-step-by-step-guide</link><guid isPermaLink="true">https://anishgandhi.com/how-to-use-sub-apps-in-bubble-the-complete-step-by-step-guide</guid><category><![CDATA[bubble.io]]></category><category><![CDATA[bubble]]></category><category><![CDATA[Bubble Developers]]></category><category><![CDATA[SaaS]]></category><category><![CDATA[multitenant]]></category><category><![CDATA[multi tenant database design]]></category><category><![CDATA[saas development ]]></category><category><![CDATA[saas development services]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Tue, 06 May 2025 01:58:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746478710286/e7a89c3a-0e54-42bd-95a2-df91259fefdc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-what-is-a-sub-app-in-bubbleio">What is a sub-app in bubble.io?</h2>
<p>A <strong>sub-app</strong> is a <em>separate app instance</em> created from your main Bubble app. It:</p>
<ul>
<li><p>It has its own <strong>database</strong> (you can optionally copy data from the main app at creation).</p>
</li>
<li><p>Has its own <strong>domain</strong>, <strong>users</strong>, <strong>privacy rules</strong>, and <strong>media uploads</strong>.</p>
</li>
</ul>
<p>Shares the <strong>editor design</strong>, <strong>workflows</strong>, and <strong>data types</strong> pushed from the main app.</p>
<hr />
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-what-is-the-need-for-sub-apps-in-bubbleio">What is the need for sub-apps in bubble.io?</h2>
<p><a target="_blank" href="http://Bubble.io">Bubble.io</a> sub-apps are not just copies of your app - they're a strong way to <strong>grow your product setup</strong>, provide <strong>custom deployments</strong>, and handle the needs of <strong>multi-client SaaS</strong>. Whether you're aiming for small businesses or large companies, sub-apps offer flexibility, security, and ease - all within Bubble’s no-code platform.</p>
<h3 id="heading-1-white-label-saas-give-each-client-their-own-branded-app"><strong>1. White-label SaaS: Give Each Client Their Own Branded App</strong></h3>
<p>One of the most common reasons to use sub-apps is to power <strong>white-labeled software</strong>.</p>
<h4 id="heading-here-is-the-problem-sub-apps-will-solve">🔧 Here is the problem sub-apps will solve:</h4>
<p>If you offer your product to multiple businesses (e.g., gyms, schools, agencies), each might want its <strong>own logo</strong>, <strong>branding</strong>, <strong>domain</strong>, and <strong>client-specific features</strong>. Managing all of this in a single app instance can become chaotic and limit scalability.</p>
<h4 id="heading-solution-with-sub-apps">✅ Solution with Sub-Apps:</h4>
<ul>
<li><p>Create a sub-app for each client from the main Bubble app.</p>
</li>
<li><p>Customize branding, themes, and domain per sub-app.</p>
</li>
<li><p>Keep the core logic consistent across all clients by pushing updates from the main app.</p>
</li>
</ul>
<p>Each client has a completely <strong>isolated database</strong>, increasing security and privacy.</p>
<h3 id="heading-2-enterprise-deployments-one-app-per-company"><strong>2. Enterprise Deployments: One App Per Company</strong></h3>
<p>Larger enterprise clients often require <strong>full data separation</strong>, enhanced security, or compliance with internal policies (like SOC 2).</p>
<h4 id="heading-here-is-the-problem-sub-apps-will-solve-1">🔧 Here is the problem sub-apps will solve:</h4>
<p>Enterprises may not want to share infrastructure with other users or want full control over data retention and access.</p>
<h4 id="heading-solution-with-sub-apps-1">✅ Solution with Sub-Apps:</h4>
<ul>
<li><p>Give each enterprise its <strong>own sub-app</strong>—technically a standalone Bubble app.</p>
</li>
<li><p>Data is not shared across apps; only the data structure and editor updates are pushed.</p>
</li>
<li><p>Enterprises can have their own deployment schedules, access policies, and even admin roles.</p>
</li>
</ul>
<p>🛡️ This gives your product a <strong>multi-instance architecture</strong> without the engineering complexity you'd typically face building it from scratch.</p>
<h3 id="heading-3-regional-versions-comply-with-local-laws-and-optimize-performance"><strong>3. Regional Versions: Comply with Local Laws and Optimize Performance</strong></h3>
<p>If your app operates across <strong>multiple countries or regions</strong>, you may need to comply with data localization laws or reduce latency by deploying closer to your users.</p>
<h4 id="heading-here-is-the-problem-sub-apps-will-solve-2">🔧 Here is the problem sub-apps will solve:</h4>
<p>Running a single app globally might violate data policies like GDPR or create latency for users far from your app’s server location.</p>
<h4 id="heading-solution-with-sub-apps-2">✅ Solution with Sub-Apps:</h4>
<ul>
<li><p>Spin up sub-apps hosted on different domains or subdomains (e.g., <a target="_blank" href="http://app.company-eu.com"><code>app.company-eu.com</code></a>, <a target="_blank" href="http://app.company-us.com"><code>app.company-us.com</code></a>).</p>
</li>
<li><p>Keep data physically and logically separated by region.</p>
</li>
<li><p>Customize privacy rules, legal content, or features based on locale.</p>
</li>
</ul>
<p>🌍 Examples:</p>
<ul>
<li>A FinTech app with separate EU and US sub-apps for GDPR and CCPA compliance.</li>
</ul>
<p>A learning platform with region-specific languages, currencies, and legal disclaimers.</p>
<hr />
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-how-do-you-create-a-sub-app-from-the-main-app">How do you create a sub-app from the main app?</h2>
<p><mark>You need to upgrade your main bubble app on the Team Plan to have the ability to create a sub-app.</mark></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746474102700/3e12df55-c9cd-4bdf-936f-ab7349eaa6cb.png" alt="Bubble.io Team Plan" class="image--center mx-auto" /></p>
<p>Once you are in Team Plan, you can create a sub-app from any branch by going to Settings → Sub apps → create a sub-app with the app name and DB Copy shown in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746474654418/a266c6fa-8291-43f1-a8f8-3e149a7a1c24.png" alt="Bubble.io path to create sub app from main app" class="image--center mx-auto" /></p>
<p>This newly created sub-app will be shown in your bubble.io account with the free plan.</p>
<ul>
<li><h3 id="heading-important-notes"><strong><mark>Important Notes:</mark></strong></h3>
<ul>
<li><p>All Team plan collaborators of the main app won’t be added to the sub-app collaborator.You need to upgrade your sub-app plan to the Growth plan to add more collaborators.</p>
</li>
<li><p>No matter which branch of the main app, you are creating a sub-app; the Sub-app will be a replica of the main branch version of the main app.</p>
</li>
<li><p>If you ‘remove’ a sub-app from the main app, it won’t be deleting an app. It will just break the connection between the main and sub-app. To delete the sub-app, you need to go to the app and delete it like you delete the regular bubble.io app.</p>
</li>
</ul>
</li>
</ul>
<ul>
<li><h3 id="heading-to-save-excessive-cost-here-is-what-i-recommend"><strong><mark>To save excessive cost, here is what I recommend</mark></strong></h3>
<p>  - Give ownership of sub-apps related development to the single developer from your team- Create all sub-apps from that developer's bubble.io account</p>
<p>  - Upgrade that bubble.io sub-app to the starter plan. So the person who needs to work on sub-apps will be working on that with the starter plan.</p>
<p>  - If you need multiple developers working on a single sub-app, then there is no other choice but to upgrade to the Growth Plan to add multiple developers as collaborators in the sub-app. - Make sure you create your business model accordingly.</p>
</li>
</ul>
<hr />
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-how-to-push-updates-to-sub-apps-from-the-main-apps">How to Push Updates to Sub-Apps from the main apps?</h2>
<p>Here’s a step-by-step guide to safely push updates to sub-apps:</p>
<h3 id="heading-step-1-create-a-dedicated-branch-for-sub-app-changes-recommended">Step 1: Create a Dedicated Branch for Sub-App Changes (Recommended)</h3>
<p>Before making changes intended for sub-apps:</p>
<ul>
<li><p>Go to the <strong>Branches</strong> tab in the Bubble editor.</p>
</li>
<li><p>Create a new branch (for example: <code>subapp-update-branch</code>).</p>
</li>
<li><p>Build and test your changes in this isolated branch.</p>
</li>
</ul>
<p>This allows you to separate sub-app-specific changes from other development work.</p>
<h3 id="heading-step-2-merge-that-branch-into-the-main-branch">Step 2: Merge That Branch into the <code>main</code> Branch</h3>
<p>Once your updates are tested and ready:</p>
<ul>
<li>Merge your working branch into the <code>main</code> branch.</li>
</ul>
<p>Only the <code>main</code> branch can push updates to sub-apps. No other branch has this ability.</p>
<h3 id="heading-step-3-push-changes-from-the-main-app-to-sub-apps">Step 3: Push Changes from the Main App to Sub-Apps</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746477682686/afb950e0-7905-4d6f-b300-0979e2d845a1.png" alt="Push changes from main app to all sub-apps" class="image--center mx-auto" /></p>
<p>Navigate to:</p>
<ul>
<li><p><strong>Settings → Sub-Apps</strong> tab</p>
</li>
<li><p>Locate the list of connected sub-apps</p>
</li>
<li><p>Click <strong>“Push changes”</strong> for each sub-app you wish to update</p>
</li>
</ul>
<p>This process will transfer:</p>
<ul>
<li><p>All <strong>editor changes</strong> (pages, workflows, logic)</p>
</li>
<li><p><strong>Data type structures</strong> (not data)</p>
</li>
</ul>
<h3 id="heading-step-4-revert-or-reset-the-main-branch-optional">Step 4: Revert or Reset the Main Branch (Optional)</h3>
<p>If your changes were only meant for sub-apps and not for your live app:</p>
<ul>
<li><p>Revert the <code>main</code> branch to its previous state, or</p>
</li>
<li><p>Reset the <code>main</code> branch to the <code>live</code> version</p>
</li>
</ul>
<p>This ensures the <code>main</code> The branch stays clean and doesn't accidentally deploy sub-app-specific changes to your production environment.</p>
<hr />
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-what-will-be-copied-from-the-main-app-to-the-sub-app-and-what-will-remain-independent-in-the-sub-app">What will be copied from the main app to the sub-app, and what will remain independent in the sub-app?</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Feature</td><td>Copied on First Push</td><td>Included in Future Pushes</td><td>Independent in Sub-App</td></tr>
</thead>
<tbody>
<tr>
<td>Editor (UI &amp; workflows)</td><td>✅</td><td>✅</td><td>❌</td></tr>
<tr>
<td>Data Types (structure)</td><td>✅</td><td>✅</td><td>❌</td></tr>
<tr>
<td>Live Database (records)</td><td>✅ (optional)</td><td>❌</td><td>✅</td></tr>
<tr>
<td>Logs, Media, Analytics</td><td>❌</td><td>❌</td><td>✅</td></tr>
<tr>
<td>Domain, App Settings</td><td>❌</td><td>❌</td><td>✅</td></tr>
</tbody>
</table>
</div><h3 id="heading-what-doesnt-get-pushed-from-the-main-app-to-the-sub-app">What doesn’t Get Pushed from the Main app to the sub-app?</h3>
<p>The following are not transferred to sub-apps during a push:</p>
<ul>
<li><p>Database records (live data)</p>
</li>
<li><p>Media files (each sub-app manages its own)</p>
</li>
<li><p>App settings, domain, and logs</p>
</li>
<li><p>Plugin-specific configurations unique to each sub-app</p>
</li>
</ul>
<hr />
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h3 id="heading-special-request-to-bubbleio-for-specific-sub-apps-setup">Special request to bubble.io for specific sub-apps setup</h3>
<p>You can request for specific settings to not propogate from main app to sub-app to Bubble team as per <a target="_blank" href="https://manual.bubble.io/help-guides/infrastructure/sub-apps">Bubble.io manual</a></p>
<p>Here is the screenshot of specific section:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746508832877/dc4da5c3-2016-409e-858d-e82b5c688672.png" alt="specific protection for sub-apps from main app request to bubble.io" class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-the-most-important-part-to-implement-in-existing-sub-apps-before-pushing-changes-from-the-main-app-to-sub-apps">The most important part to implement in existing sub-apps before pushing changes from the main app to sub-apps:</h2>
<ul>
<li><p>Create a save point in existing sub-apps before the push.</p>
</li>
<li><p>When you push to all sub-apps, it won’t raise any conflict with existing sub-app changes that you made separately, and will make all Editor (UI &amp; workflows) and Data Types (structure) the same as the main-app, which you didn’t want for all sub-apps; you will be able to restore your created save point and figure out another way to implement some of the necessary changes from the main-app, but it will save your independent changes made in sub-apps.</p>
</li>
</ul>
<hr />
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-best-practices-and-tips-for-working-with-sub-apps-in-bubbleiohttpbubbleio">Best Practices and Tips for Working with Sub-Apps in <a target="_blank" href="http://Bubble.io">Bubble.io</a></h2>
<p><a target="_blank" href="http://bubble.io/">Using s</a>ub-apps effectively requires not just knowing how to push updates, but also following a disciplined approach to avoid confusion, data issues, or deployment errors.</p>
<p>1. Use Clear Naming Conventions for Sub-Apps</p>
<p>As your number of your sub-apps grows, it’s easy to lose track of which sub-app is for which client, region, or use case.</p>
<p><strong>Recommendation:</strong></p>
<ul>
<li><p>Include the purpose or client name in the sub-app name, such as <code>client-xyz-app</code>, <code>staging-eu</code>, or <code>demo-app-v2</code>.</p>
</li>
<li><p>Maintain a naming convention that clearly distinguishes between production, staging, and testing environments.</p>
</li>
</ul>
<p><strong>Why it matters:</strong><br />It improves collaboration, simplifies documentation, and reduces the risk of pushing updates to the wrong sub-app.</p>
<p>2. Keep the Main App Clean — Use Branches for Development</p>
<p>The main app should only reflect the version you want all sub-apps to inherit. Avoid building or testing new features directly in the <code>main</code> branch.</p>
<p><strong>Recommendation:</strong></p>
<ul>
<li><p>Create dedicated branches for feature development or sub-app-specific changes.</p>
</li>
<li><p>Merge only the final, stable version into <code>main</code> when you are ready to push to sub-apps.</p>
</li>
</ul>
<p><strong>Why it matters:</strong><br />This minimizes the risk of deploying unstable code across all sub-apps and keeps your main app production-ready at all times.</p>
<p>3. Always Document What Was Pushed and Why</p>
<p>When you push changes to a sub-app, there’s no automated log that tracks what was updated or why. This can lead to confusion, especially in teams.</p>
<p><strong>Recommendation:</strong></p>
<ul>
<li><p>Maintain a change log (in Linear, Notion, Google Docs, Git-style README, etc.).</p>
</li>
<li><p>Include: what was changed, which sub-app(s) were affected, and who made the push.</p>
</li>
</ul>
<p><strong>Why it matters:</strong><br />It helps with debugging, accountability, team communication, and audit trails.</p>
<p>4. Backup Your Main App Before Major Pushes</p>
<p>Pushing updates to sub-apps is irreversible—you can’t “undo” a push once it’s done. If something breaks, recovery becomes harder.</p>
<p><strong>Recommendation:</strong></p>
<ul>
<li><p>Before pushing major changes to sub-apps, make a manual backup of your main app by duplicating it or exporting the app JSON.</p>
</li>
<li><p>You can also use Bubble’s built-in version control to create save points.</p>
</li>
</ul>
<p><strong>Why it matters:</strong><br />Having a backup gives you peace of mind and a fallback option in case something goes wrong during deployment.</p>
<hr />
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<p>Sub-apps in <a target="_blank" href="http://Bubble.io">Bubble.io</a> give you a clean, scalable way to serve different clients, regions, or testing environments - all while keeping your core logic centralized.</p>
<p>In this guide, we covered not just <strong>how to set up and push updates to sub-apps</strong>, but also <strong>best practices to avoid breaking your main app</strong>.</p>
<p>Whether you're building a white-label SaaS, serving enterprise clients, or working across global markets, <strong>sub-apps can save you time, reduce complexity, and improve data isolation.</strong></p>
<p>Use branches, document your pushes, and back up before major changes, and you’ll be well on your way to building a stable, scalable no-code infrastructure.</p>
<p><strong><mark>Got questions? Ask in the comments. To support me, buy me a coffee.</mark></strong></p>
]]></content:encoded></item><item><title><![CDATA[How to sort a repeating group in Bubble.io by a nested field?]]></title><description><![CDATA[Sorting a repeating group in Bubble.io is straightforward until you run into nested fields.
Let’s say you’re displaying a list of Works, and each work has a related Work_satellite data type. Now, you want to sort your repeating group of Work by the W...]]></description><link>https://anishgandhi.com/how-to-sort-a-repeating-group-in-bubble-by-a-nested-field</link><guid isPermaLink="true">https://anishgandhi.com/how-to-sort-a-repeating-group-in-bubble-by-a-nested-field</guid><category><![CDATA[sorting]]></category><category><![CDATA[bubble.io]]></category><category><![CDATA[No Code]]></category><category><![CDATA[UIUX]]></category><category><![CDATA[bubble sort]]></category><category><![CDATA[Bubble Developers]]></category><category><![CDATA[bubble]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Thu, 01 May 2025 09:03:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746054046612/03d09a17-ad51-426b-92fd-731ab18e45c2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Sorting a repeating group in <a target="_blank" href="http://Bubble.io">Bubble.io</a> is straightforward until you run into nested fields.</p>
<p><strong>Let’s say you’re displaying a list of Works, and each work has a related Work_satellite data type. Now, you want to sort your repeating group of Work by the Work_satellite’s last modified date.</strong></p>
<p><strong>This is where Bubble’s built-in sorting options hit a wall, because you can't directly sort a repeating group by a nested field like</strong> <code>Work's Work_satellite's Modified date</code><strong>.</strong></p>
<p>But don't worry. In this article, I’ll walk you through the step-by-step process to do it.  </p>
<h2 id="heading-step-1-load-the-repeating-groups-data-in-the-pop-up">Step 1: Load the Repeating Group’s data in the pop-up</h2>
<p>In this example, I am loading the data of work in a pop-up</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746048828460/cf9f2a13-2b34-48a3-ba46-43772ffa755a.png" alt="Interface showing an Elements Tree with a highlighted &quot;Popup Hidden Variables&quot; section containing &quot;RG work,&quot; connected to a settings panel. The panel displays options for &quot;Type of content&quot; set to &quot;Work&quot; and &quot;Data source&quot; for &quot;Search for Works.&quot;" class="image--center mx-auto" /></p>
<h2 id="heading-step-2-install-floppy-localstorage-list-shifter-plugin">Step 2: Install ‘Floppy: localStorage, List Shifter’ plugin</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746050833472/f941e130-c751-4034-916e-d233220a45b1.png" alt="A product page for the &quot;Floppy: localStorage, List Shifter&quot; plugin, priced at $19 or $12/month. It has a 3.4-star rating from 14 reviews and 3.2K installs. The plugin details describe its functions for managing browser storage and manipulating lists. It includes contributor details for Keith from GRUPZ." class="image--center mx-auto" /></p>
<h2 id="heading-step-3-set-up-list-shifter-as-the-data-source-of-the-repeating-group-of-the-page">Step 3: Set up List Shifter as the data source of the Repeating Group of the page</h2>
<p>Place the ‘List Shifter Pro’ element on the page and provide RG Work’s data as a List to shift shown in the image below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746051260311/cc75515f-c080-4b02-81d8-47abe3744a58.png" alt="Screenshot of a UI builder interface showing the elements tree and settings for &quot;ListShifterPRO Work.&quot; An arrow highlights the &quot;List to Shift&quot; option, which is set to &quot;RG work's List of Works.&quot; The layout includes options for appearance and list manipulation functions like using scalars and rotating lists." class="image--center mx-auto" /></p>
<p>Now, place the ‘List Shifter’s shifted list’ as data source of the Repeating Group, which is on the page, and where you want to execute the sorting by field’s field, or in our case <code>Work's Work_satellite's Modified date</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746051694828/8f8f30ab-0ac4-4c75-a6a3-bd88224095c9.png" alt="Screenshot of a UI Builder interface showing an &quot;Elements Tree&quot; on the left with a highlighted &quot;RepeatingGroup Work&quot; layer. On the right, there's a panel with settings for the repeating group, including content type and data source details. A red arrow and label highlight the repeating group." class="image--center mx-auto" /></p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-step-4-set-up-a-workflow-for-sorting-by-the-fields-field-or-the-nested-field">Step 4: Set up a workflow for sorting by the field’s field or the Nested field</h2>
<p>You can either do the sorting by default when the Repeating Group is visible on page load, or you can execute the sorting when the user selects it via a dropdown or some other way.</p>
<p>I am setting this up as a custom event, which you can trigger on page load or when the user selects this nested field’s sorting.</p>
<p>I have created a custom event named ‘sort-list’ and added the action from the List Shifter plugin named ‘Sort List a List Shifter Pro</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746052351192/6185e1e9-de88-459c-9621-b4d4a815929f.png" alt="Interface of a workflow editor displaying a sorting process. The left panel shows settings for sorting a list using &quot;ListShifterPRO,&quot; with options like type of sort, sorting by date, and descending order. The right panel outlines a custom event triggering the sort. The background is a deep purple." class="image--center mx-auto" /></p>
<p>As you can see in the image above**, I want to sort the RG data via modified data of nested field, that is why I have selected ‘sort as’ and ‘type of sort by list’ as date.  </p>
<p>In the ‘sort by List’, I have selected<strong> <code>RG Work’s List of Work’ each item’s work_satellite’s modified date</code></strong>. Here, RG Work is the repeating group from the popup, not from an on-page repeating group.**  </p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-step-5-show-the-repeating-group-after-nested-sorting-is-completed">Step 5: Show the Repeating Group after nested sorting is completed</h2>
<p>There is an event from the List Shifter plugin named ‘ListShifterPRO WorkListShifterPRO Sort Complete’ that will be triggered once the custom event of sorting is executed and sorting is completed. On completion, you can now show on the page the Repeating Group with sorting by <code>Work's Work_satellite's Modified date</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746052966144/dde51d17-963e-4c73-9040-80b27149e28e.png" alt="A workflow diagram for &quot;ListShifterPRO Work Sort Complete&quot; with steps to show &quot;RepeatingGroup Work&quot; when the list count is greater than or equal to one, and &quot;Group Empty State&quot; when the count is less than one." class="image--center mx-auto" /></p>
<p><strong>This is how you sort a repeating group in</strong> <a target="_blank" href="http://Bubble.io"><strong>Bubble.io</strong></a> <strong>by a nested field. If you find this article helpful, you can buy me a coffee and show your support and appreciation.</strong></p>
]]></content:encoded></item><item><title><![CDATA[Step-by-Step Guide to Building an AI Assistant with Bubble.io (No Plugins)]]></title><description><![CDATA[In the fast-moving world of entrepreneurship and app development, innovation is crucial. For those exploring Bubble.io, there are endless possibilities, especially for creating an AI assistant without plugins. Whether you're an entrepreneur wanting t...]]></description><link>https://anishgandhi.com/how-to-building-an-ai-assistant-with-bubble-without-plugins</link><guid isPermaLink="true">https://anishgandhi.com/how-to-building-an-ai-assistant-with-bubble-without-plugins</guid><category><![CDATA[AI Assistants ]]></category><category><![CDATA[AIAssistant]]></category><category><![CDATA[AI assistant]]></category><category><![CDATA[bubble.io]]></category><category><![CDATA[bubble]]></category><category><![CDATA[No Code]]></category><category><![CDATA[nocode]]></category><category><![CDATA[ no-code platform]]></category><category><![CDATA[no code development]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Tue, 17 Dec 2024 10:42:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1734332610728/d928d977-7054-459f-8ba5-c6f02c15c2bd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the fast-moving world of entrepreneurship and app development, innovation is crucial. For those exploring <a target="_blank" href="http://Bubble.io">Bubble.io</a>, there are endless possibilities, especially for creating an AI assistant without plugins. Whether you're an entrepreneur wanting to improve your app or an experienced <a target="_blank" href="http://Bubble.io">Bubble.io</a> developer looking to try new things, this guide will help you build a smart assistant using only APIs. Get ready to turn your ideas into reality and take your app to the next level!</p>
<p>Here are some important terminology:</p>
<h2 id="heading-what-is-an-ai-assistant">What is an AI Assistant?</h2>
<p>An OpenAI Assistant is a tool that uses AI to have conversations. It's built with advanced language models like OpenAI’s GPT, which can understand and create natural language responses. You can add it to websites, apps, or platforms to do things like answer customer questions, create content, help with learning, increase productivity, and provide entertainment. In <a target="_blank" href="http://Bubble.io">Bubble.io</a>, an OpenAI Assistant can improve apps by adding features like automatic replies, personalized suggestions, and task automation, giving users a smooth and smart experience.</p>
<p>Here is the setup:</p>
<h2 id="heading-step-1-log-in-to-open-ai-and-create-an-assistant">Step 1: Log in to Open AI and create an assistant</h2>
<p>Create an open AI account on <a target="_blank" href="https://platform.openai.com/playground/complete">https://platform.openai.com/playground/complete</a></p>
<p>On Playground, Create a new assistant with system instructions. For example, Here I am creating a travel assistant which you can see in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734316752831/7762413b-2680-4f38-90b1-d19ef2f4f873.png" alt="Create AI Assistant and get assistant ID" class="image--center mx-auto" /></p>
<p>On assistant creation, the assistant ID will be automatically generated by bubble.io which we will use later.</p>
<h2 id="heading-step-2-generate-secret-api-tokenkey-in-open-ai">Step 2: Generate Secret API Token/Key in Open AI</h2>
<p>Go to: <a target="_blank" href="https://platform.openai.com/settings/organization/api-keys">https://platform.openai.com/settings/organization/api-keys</a><br />Create a new API key there by clicking the button shown in the next picture</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734317229126/f658415d-ebab-4788-820d-858eda2f13da.png" alt="Get Open AI API key" class="image--center mx-auto" /></p>
<h2 id="heading-step-3-setup-api-secret-key-in-bubble">Step 3: Setup API Secret key in Bubble</h2>
<p>Now go to api connector in your bubble.io app and set the Open AI secret key in the Header for all open AI calls.</p>
<h3 id="heading-what-does-the-private-key-in-the-header-mean-in-the-bubbleio-api-connector">What does the Private key in the Header mean in the bubble.io api connector?</h3>
<p>This method involves adding the key to the header of all requests, usually under the parameter name <em>Authorization</em>, though you can use a custom name as specified in the external API documentation if necessary.</p>
<p>Below image and explanation of the image were taken from <a target="_blank" href="https://manual.bubble.io/help-guides/integrations/api/the-api-connector/authentication">the bubble.io manual</a>.</p>
<p><img src="https://manual.bubble.io/~gitbook/image?url=https%3A%2F%2F34394582-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-M5sbzwG7CljeZdkntrL%252Fuploads%252F2jKVrSFvrvlJlJpIkGOj%252Fkey-in-header.png%3Falt%3Dmedia%26token%3D38978585-3708-41c2-903b-4a4814bb2e40&amp;width=768&amp;dpr=4&amp;quality=100&amp;sign=e304c948&amp;sv=2" alt="Private key in header in bubble.io" /></p>
<ol>
<li><p>The first is the name of the parameter the server needs.</p>
</li>
<li><p>The second is the actual API key for the <strong>development</strong> version of your app.</p>
</li>
<li><p>The third is the API key for the <strong>live</strong> version of your app.</p>
</li>
</ol>
<p><strong>What does a private key in the header look like?</strong></p>
<pre><code class="lang-javascript">Authorization: Bearer &lt;token&gt;
</code></pre>
<p>The example above would be one <em>row</em> of the header – it also contains other data. The <em>&lt;token&gt;</em> is replaced by the actual token without the &lt;&gt; signs just like in our example below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734320942324/13b21bab-f7b6-480d-826c-7882db987af3.png" alt="Screenshot of an API configuration panel with fields for API name, authentication type, key name, and development key value. Includes shared headers for all calls." class="image--center mx-auto" /></p>
<h3 id="heading-what-are-shared-headers-for-api-calls">What are shared headers for api calls?</h3>
<p>In <a target="_blank" href="http://Bubble.io">Bubble.io</a>, the "shared header" in the API Connector lets you set common HTTP headers that will be automatically added to all API requests made through a specific API connection. This is helpful for headers like authentication tokens, API keys, or content-type specifications needed for every API request.</p>
<h3 id="heading-how-an-api-call-with-shared-headers-works">How an API Call with Shared Headers Works:</h3>
<ol>
<li><p><strong>Shared Headers</strong>: Headers set in the shared header section are included in every HTTP request for all API calls using that connection.</p>
</li>
<li><p><strong>Endpoint-Specific Headers</strong>: You can add custom headers for individual API calls or endpoints, which will be combined with the shared headers.</p>
</li>
<li><p><strong>Complete API Request</strong>: When you make an API request, Bubble combines the shared headers with any endpoint-specific headers you add. The request is then sent with all necessary headers.</p>
</li>
</ol>
<p>This setup ensures your API calls are consistent and reduces the need to repeatedly define headers, making API integration easier.</p>
<h2 id="heading-step-4-understand-how-the-ai-assistant-will-work-via-api">Step 4: Understand How the AI Assistant will work via API</h2>
<p>Here is the flowchart of the whole process:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734334734603/bd946baa-4fa7-4487-bb93-0da6b3d950bb.png" alt="Flowchart illustrating the process of creating and managing an assistant of OpenAI in Bubble.io APP" class="image--center mx-auto" /></p>
<p>We have already created an assistant in step 1 that provided us assistant ID.</p>
<ul>
<li><p>Save the assistant ID in the database for us to use in API Calls</p>
</li>
<li><p>Creating a Communication Thread: With the assistant ready and its ID safely stored, it's time to create a thread for organizing interactions. Think of this thread as a conversation space where all messages between the user and the assistant are contained, making it easier to track and manage communications.</p>
</li>
<li><p>Creating a Message: Within the newly created thread, you can now create a message as a user for the assistant. Basically whatever users write to AI will be created as a message. Understand this, creating a message and sending a message to AI are both different things. Right now, we have just created a message, and haven’t sent it to AI yet.</p>
</li>
<li><p>Creating a Run: To process the message, a "run" is created. This involves executing any tasks or computations necessary to respond to the user. The run is essentially the action engine, driving the assistant's functionality. In easy language, when the run is created means we have sent a message to AI and now AI is processing it.</p>
</li>
<li><p>Monitoring the Run Status: Once the run is initiated, it's crucial to monitor its progress. This involves retrieving the run and checking the current status of the run and whether the run has been completed. If it’s still in progress, our bubble.io app will continue to check the status until completion.</p>
</li>
<li><p>Retrieving All Messages: Upon successful completion of the run, the final step is to retrieve all messages within the thread. Once we receive the messages, we will show those messages to the user and then the user can create more messages to interact with an assistant if the user wants.</p>
</li>
</ul>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-step-5-api-setup-in-bubble">Step 5: API Setup in Bubble</h2>
<p>Assistant is created in open AI and we have to get the assistant ID from there.</p>
<p>Now Let’s set the following APIs in API Connector</p>
<ul>
<li><h3 id="heading-create-thread">Create Thread</h3>
</li>
</ul>
<p>API Documentation: <a target="_blank" href="https://platform.openai.com/docs/api-reference/threads/createThread">https://platform.openai.com/docs/api-reference/threads/createThread</a><br />The Bubble.io API Connector setup is shown in the image below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734420678226/6345f069-b7e2-481a-ba0b-d661f2b107af.png" alt="Open AI - Create Thread API Setup in bubble.io" class="image--center mx-auto" /></p>
<p>From a successful API response, you will get a thread ID, Save it somewhere because we will need it in other API calls.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734421242915/dc671ffb-5318-45dd-adf0-eb2bb05ae9c7.png" alt="Open AI Create thread successful api in bubble.io" class="image--center mx-auto" /></p>
<ul>
<li><h3 id="heading-create-a-message">Create a Message</h3>
</li>
</ul>
<p>API Documentation: <a target="_blank" href="https://platform.openai.com/docs/api-reference/messages/createMessage">https://platform.openai.com/docs/api-reference/messages/createMessage</a></p>
<p>The thread ID we saved earlier will be used here.</p>
<p>The Bubble.io API Connector setup is shown in the image below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734421429558/ea1eb8eb-c073-4839-b250-f7dd3c6ac120.png" alt="Open AI Create Message API Setup in bubble.io" class="image--center mx-auto" /></p>
<ul>
<li><h3 id="heading-create-run">Create Run</h3>
</li>
</ul>
<p>API Documentation: <a target="_blank" href="https://platform.openai.com/docs/api-reference/runs/createRun">https://platform.openai.com/docs/api-reference/runs/createRun</a></p>
<p>The thread ID and assistant ID we saved earlier will be used here.</p>
<p>The Bubble.io API Connector setup is shown in the image below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734422242798/0210e826-ad64-4ced-9e0e-ba5639077f41.png" alt="Open AI create run API setup in bubble.io" class="image--center mx-auto" /></p>
<p>Now from the successful API Call, Save the Run ID.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734423771683/dddd0530-cfea-42b4-ac6f-92dabbdbf2c5.png" alt="Open AI create Run successful API Initialisation in bubble.io" class="image--center mx-auto" /></p>
<ul>
<li><h3 id="heading-retrieve-run">Retrieve Run</h3>
</li>
</ul>
<p>API Documentation: <a target="_blank" href="https://platform.openai.com/docs/api-reference/runs/getRun">https://platform.openai.com/docs/api-reference/runs/getRun</a></p>
<p>The thread ID and run ID we saved earlier will be used here.</p>
<p>The Bubble.io API Connector setup is shown in the image below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734423123234/81535a61-985d-4030-a0c3-ec2c57d299ee.png" alt="Open AI Retrive run API call setup in bubble.io" class="image--center mx-auto" /></p>
<ul>
<li><h3 id="heading-list-message">List Message</h3>
</li>
</ul>
<p>API Documentation: <a target="_blank" href="https://platform.openai.com/docs/api-reference/messages/listMessages">https://platform.openai.com/docs/api-reference/messages/listMessages</a></p>
<p>The thread ID we saved earlier will be used here.</p>
<p>The Bubble.io API Connector setup is shown in the image below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734423531282/8ec73018-5c50-4f9f-853d-2f141f213be3.png" alt="Open AI retrive all list messages api setup in bubble.io" class="image--center mx-auto" /></p>
<p>With this, all the essential APIs are set in bubble.io</p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-step-6-make-open-ai-api-calls-more-secure-in-a-bubbleio">Step 6: Make Open AI API Calls More Secure in a bubble.io</h2>
<p>Even if your run ID, assistant ID, and thread ID are public. They are useless without an Open AI secret key which we are not exposing here in all API calls. The secret key is set as a private key in the header so your api calls are secure.<br />To make your api calls more secure, check this: <a target="_blank" href="https://anishgandhi.com/bubble-security-api-best-practices">Bubble.io API Call Security Best Practices</a></p>
<h2 id="heading-step-7-ui-setup-in-bubble">Step 7: UI Setup in Bubble</h2>
<ul>
<li>Prepared a basic setup with the Start button shown below</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734424564803/b9e4f8f0-45ad-424f-837e-fe8e8c97417c.png" alt="A screen displaying the text &quot;Let's decide your travel destination&quot; with a &quot;Start&quot; button below it." class="image--center mx-auto" /></p>
<ul>
<li>Some loading for UX when APIs are in execution</li>
</ul>
<p>Create a chat Interface with multiline input to write a message, make sure we can with UI we can identify responses from the user and assistant.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734426684839/e08998c9-109e-418d-8563-401f08bd110e.png" alt="A chat UI for open AI and user conversation for ai assistant" class="image--center mx-auto" /></p>
<p><strong>UI creation conceptualization was assisted by <a class="user-mention" href="https://hashnode.com/@dixitpatel">Dixit Patel</a></strong></p>
<h2 id="heading-step-8-workflow-setup-in-bubble">Step 8: Workflow Setup in Bubble</h2>
<ul>
<li><p>Create an option set to track run status with three options: Not Running, In Progress, Completed</p>
<ul>
<li><p>not running is a default status</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734427822006/f3183f64-c213-4816-912f-1df521c70add.png" alt="Custom state in Bubble.io to track run status from Retrieve run API call" class="image--center mx-auto" /></p>
</li>
</ul>
</li>
<li><p>Create custom states on the page level that store: thread ID, run ID, run status, and list of messages (List Messages API data type)</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734428111780/c65a478e-7fef-438c-b4e0-f92bdeb614ed.png" alt="custom states on the page level that store: thread ID, run ID, run status, and list of messages (List Messages API data type)" class="image--center mx-auto" /></p>
</li>
<li><p>Add a simple looper plugin from <a target="_blank" href="https://bubble.io/plugin/simple-looper-workflow-repeater-1618892957212x885363265747026000">here</a></p>
</li>
</ul>
<p>On the Start button (Shown in UI) run the following workflows</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734428869092/ec30cc64-9901-43f5-a685-5a5d99247a31.png" alt="Bubble.io workflow when chat with open AI assistant initiated " class="image--center mx-auto" /></p>
<p>Then,</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734429323526/08f32e6e-6f15-4ea0-9a9c-307b770755e0.png" alt="Workflow in bubble.io when simple looper is running" class="image--center mx-auto" /></p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<ul>
<li><p>Once the loop is finished, here is what happens</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734429515760/33a926db-713f-421a-b53d-07db751f2008.png" alt="Workflow in bubble.io when simple looper is finished" class="image--center mx-auto" /></p>
</li>
<li><p>Then load all the messages into a custom state or database (your choice)</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734429841257/de7d3e0b-d6d1-4592-a2b2-8f3d4d2ab7b7.png" alt="Message retrieval from Open AI via API in bubble.io" class="image--center mx-auto" /></p>
</li>
</ul>
<p>Till now, you have seen the workflow execution from the start button. Now if this is executed from multiline input of chat, then the workflow will be similar with some minor changes (shown in the image below)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734430288648/dcfe3a48-7585-470d-8c70-0628a2f125f0.png" alt="dynamic message chat with AI Assistant via bubble.io" class="image--center mx-auto" /></p>
<h2 id="heading-step-9-differntiate-user-and-assistant-messages">Step 9: Differntiate User and Assistant Messages</h2>
<p>Now Just show the messages in chat UI created either via custom state or via database.</p>
<p>In my example, I have shown messages in Repeating Group Chat which I set like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734431385050/6bc2a52c-430e-4888-9d93-555f61db89dd.png" alt class="image--center mx-auto" /></p>
<p>To identify the messages from the User and assistant, you can use roles to differentiate</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734431588320/c03a51ef-f1d1-4260-84ec-820dbbb36a56.png" alt class="image--center mx-auto" /></p>
<p>These step-by-step instructions cover everything from setting up your OpenAI account and generating API keys to configuring API calls and designing a user-friendly interface. By following these steps, you can create a sophisticated AI assistant that elevates your app's functionality and user experience, paving the way for innovation and efficiency in your projects.</p>
]]></content:encoded></item><item><title><![CDATA[5 Simple Scripts to Enhance UI/UX for Your Bubble.io App]]></title><description><![CDATA[How to Customise the bubble.io app scrollbar? (No Plugins)
Bubble.io’s default scrollbar doesn’t give the desired UI/UX your app needs. Rather than using any plugins, you can write code to achieve the scrollbar style you would like.
You can use this ...]]></description><link>https://anishgandhi.com/5-simple-scripts-to-upgrade-your-bubble-apps-ui-and-ux</link><guid isPermaLink="true">https://anishgandhi.com/5-simple-scripts-to-upgrade-your-bubble-apps-ui-and-ux</guid><category><![CDATA[bubble.io]]></category><category><![CDATA[bubble]]></category><category><![CDATA[UI]]></category><category><![CDATA[UX]]></category><category><![CDATA[Bubble Developers]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[CSS]]></category><category><![CDATA[No Code]]></category><category><![CDATA[nocode]]></category><category><![CDATA[ no-code platform]]></category><category><![CDATA[no code development]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Tue, 10 Dec 2024 06:49:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733811263157/44bd46be-e4b0-42ec-b9c1-14a8ba73cdfd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-how-to-customise-the-bubbleio-app-scrollbar-no-plugins">How to Customise the bubble.io app scrollbar? (No Plugins)</h2>
<p>Bubble.io’s default scrollbar doesn’t give the desired UI/UX your app needs. Rather than using any plugins, you can write code to achieve the scrollbar style you would like.</p>
<p>You can use this code in multiple places depending on your use cases.</p>
<ul>
<li><p>To apply the scrollbar styling to the whole app: Go to settings → SEO/metatags → Script in Body from your bubble.io editor and write the code mentioned below.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733803665278/45a47e88-604c-444c-a48e-6622856af834.png" alt="Screenshot of a bubble.io settings showing &quot;Advanced settings&quot; with sections for &quot;Script/meta tags in header&quot; and &quot;Script in the body.&quot; There is example CSS code for customizing a scrollbar." class="image--center mx-auto" /></p>
</li>
<li><p>To apply the scrollbar styling on the whole page level: Go to Page on bubble.io editor, open page properties, and write the code mentioned below on Page HTML Header</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733804237952/1cc7c4a1-cb1f-4e40-b24c-2f5dd931d44d.png" alt="Screenshot of a UI Bubble.io interface showing an element tree with a highlighted &quot;index&quot; element. A text description and a logout button are visible. A panel displays HTML styling code related to scrollbars." class="image--center mx-auto" /></p>
<p>  Here is the code that you can copy directly and paste into your app.</p>
</li>
</ul>
<pre><code class="lang-css">
&lt;<span class="hljs-selector-tag">style</span>&gt;
<span class="hljs-comment">/* Customize the scrollbar */</span>
<span class="hljs-selector-pseudo">::-webkit-scrollbar</span> {
  <span class="hljs-attribute">width</span>: <span class="hljs-number">2px</span>; <span class="hljs-comment">/* Width of the scrollbar, you can modify the number */</span>
}

<span class="hljs-selector-pseudo">::-webkit-scrollbar-thumb</span> {
  <span class="hljs-attribute">background</span>: <span class="hljs-number">#000000</span>; <span class="hljs-comment">/* Scrollbar thumb color, you can modify the color hex code */</span>
  <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">5px</span>; <span class="hljs-comment">/* Rounded edges for the thumb, you can modify the number */</span>
}

<span class="hljs-selector-pseudo">::-webkit-scrollbar-thumb</span><span class="hljs-selector-pseudo">:hover</span> {
  <span class="hljs-attribute">background</span>: <span class="hljs-number">#FFFFFF</span>; <span class="hljs-comment">/* Thumb color on hover,you can modify the color hex code */</span>
}

<span class="hljs-selector-pseudo">::-webkit-scrollbar-track</span> {
  <span class="hljs-attribute">background</span>: <span class="hljs-number">#FFFFFF</span>; <span class="hljs-comment">/* Scrollbar track color,you can modify the color hex code */</span>
  <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">5px</span>; <span class="hljs-comment">/* Rounded edges for the track,you can modify the number */</span>
}


&lt;/<span class="hljs-selector-tag">style</span>&gt;
</code></pre>
<ul>
<li><p>Now If you want to modify the styling of the Repeating Group Scrollbar or the scrollbar of specific components on the page, Here is the code</p>
<pre><code class="lang-css">  &lt;<span class="hljs-selector-tag">style</span>&gt;
  <span class="hljs-comment">/* Customize the scrollbar for the Repeating Group */</span>
  <span class="hljs-selector-id">#custom-scrollbar</span> <span class="hljs-selector-pseudo">::-webkit-scrollbar</span> {
    <span class="hljs-attribute">width</span>: <span class="hljs-number">2px</span>; <span class="hljs-comment">/* Width of the scrollbar */</span>
  }

  <span class="hljs-selector-id">#custom-scrollbar</span> <span class="hljs-selector-pseudo">::-webkit-scrollbar-thumb</span> {
    <span class="hljs-attribute">background</span>: <span class="hljs-number">#000000</span>; <span class="hljs-comment">/* Scrollbar thumb color */</span>
    <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">5px</span>; <span class="hljs-comment">/* Rounded edges for the thumb */</span>
  }

  <span class="hljs-selector-id">#custom-scrollbar</span> <span class="hljs-selector-pseudo">::-webkit-scrollbar-thumb</span><span class="hljs-selector-pseudo">:hover</span> {
    <span class="hljs-attribute">background</span>: <span class="hljs-number">#FFFFFF</span>; <span class="hljs-comment">/* Thumb color on hover */</span>
  }

  <span class="hljs-selector-id">#custom-scrollbar</span> <span class="hljs-selector-pseudo">::-webkit-scrollbar-track</span> {
    <span class="hljs-attribute">background</span>: <span class="hljs-number">#FFFFFF</span>; <span class="hljs-comment">/* Scrollbar track color */</span>
    <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">5px</span>; <span class="hljs-comment">/* Rounded edges for the track */</span>
  }
  &lt;/<span class="hljs-selector-tag">style</span>&gt;
</code></pre>
<p>  Enable the <strong>ID Attribute</strong> option in your app settings:</p>
<ul>
<li><p>Go to <strong>Settings &gt; General</strong>.</p>
</li>
<li><p>Check the box for <strong>Expose the option to add an ID attribute to HTML elements</strong>.</p>
</li>
<li><p>Now Add an <strong>HTML element</strong> to the page and paste the modified code into the HTML element.</p>
</li>
<li><p>In the Repeating Group's properties, assign the ID attribute as <code>custom-scrollbar</code></p>
</li>
</ul>
</li>
</ul>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-how-to-create-a-custom-picture-uploader-or-file-uploader-in-bubbleio-no-paid-plugins">How to Create a Custom Picture Uploader or File Uploader in Bubble.io? <strong>(No Paid Plugins)</strong></h2>
<p>There are many style limitations for picture uploaders and file uploaders in bubble.io. So you would like to design a custom uploader using groups, icons, and text elements as per your requirements but on clicking that custom uploader, it should work the same as if a user is clicking on picture Uploader or file uploader.</p>
<p>For example, here is the custom uploader I designed using group, text, and icon elements:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733806087799/88ff9267-60d7-4746-ab35-8c7a4f6d37f9.png" alt="How to Create a Custom Picture Uploader or File Uploader in Bubble.io?" class="image--center mx-auto" /></p>
<p>But on the element level, there is a tiny picture uploader inside:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733806203198/8fcdac5f-7ee9-4b99-8334-66824a500947.png" alt="How to Create a Custom Picture Uploader or File Uploader in Bubble.io?" class="image--center mx-auto" /></p>
<p>Now On clicking on the group certificate image, I want the UX to be the same as if the user is clicking on the picture uploader. Here is how to achieve this:</p>
<ul>
<li><p>Install the Toolbox plugin in the bubble.io app</p>
</li>
<li><p>On click of Group Certificate Image, set the ‘Run Javascript’ action with the following code</p>
<pre><code class="lang-javascript">  <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"education"</span>).getElementsByTagName(<span class="hljs-string">"input"</span>)[<span class="hljs-number">0</span>].click();
</code></pre>
</li>
<li><p>Place the ID Attribute ‘education’ on the element that you would like to simulate on click, in our case, it is the picture uploader</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733806703569/e677ee27-27ed-403b-89eb-712d00fea8ba.png" alt="How to Create a Custom Picture Uploader or File Uploader in Bubble.io?" class="image--center mx-auto" /></p>
<p>  So clicking on the group will work as if the user has clicked on the picture uploader.</p>
</li>
</ul>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-how-to-prevent-page-scrolling-when-popup-is-visible-in-bubbleio-no-paid-plugins">How to prevent page scrolling when popup is visible in bubble.io? (No Paid Plugins)</h2>
<p>Here are the reasons why this is necessary:</p>
<ul>
<li><p><strong>Problem:</strong> If the user scrolls while the popup is open, the background content moves, which can be distracting. In Bubble, the popup might stay in place while the background moves, causing visual issues like misaligned elements. Scrollable backgrounds can confuse users using keyboards or screen readers, as they might interact with the wrong elements. If the page scrolls behind a popup, it can mess up the layout, especially for apps with parallax effects or sticky elements. Users might accidentally click or interact with background elements while scrolling, leading to unintended actions.</p>
</li>
<li><p><strong>Solution:</strong> Disabling scrolling ensures the background remains static, keeps the user focused on the popup's content, prevents design inconsistencies, and avoids unintended interactions.</p>
</li>
</ul>
<p><strong>Here is how to achieve this:</strong></p>
<ul>
<li><p>Install Toolbox Plugin</p>
</li>
<li><p>Create a ‘Do when condition is true’ workflow and Put condition that this will trigger everytime when the popup is visible.</p>
</li>
<li><p>On trigger of this workflow, set the ‘Run Javascript’ action with the following code.</p>
</li>
<li><p>Place the ID Attribute ‘experience’ on the popup</p>
<p>  With this, Scrolling is disabled for the background but enabled for the popup content if it overflows the given height.</p>
</li>
</ul>
<pre><code class="lang-javascript"><span class="hljs-comment">// This is every time the popup is visible</span>

  $(<span class="hljs-string">"#experience"</span>).wrap( <span class="hljs-string">"&lt;div class='innerScroll'&gt;&lt;/div&gt;"</span> );

  $(<span class="hljs-string">".innerScroll"</span>).css({<span class="hljs-string">'overflow-y'</span>:<span class="hljs-string">'auto'</span>,<span class="hljs-string">'position'</span>:<span class="hljs-string">'fixed'</span>,<span class="hljs-string">'top'</span>:<span class="hljs-string">'0'</span>, <span class="hljs-string">'left'</span>:<span class="hljs-string">'0'</span>, <span class="hljs-string">'right'</span>:<span class="hljs-string">'0'</span>,<span class="hljs-string">'bottom'</span>:<span class="hljs-string">'0'</span>,<span class="hljs-string">'z-index'</span>:<span class="hljs-string">'10000'</span>});

  $(<span class="hljs-string">"body"</span>).css(<span class="hljs-string">'overflow'</span>,<span class="hljs-string">'hidden'</span>);

  <span class="hljs-keyword">var</span> esc = $.Event(<span class="hljs-string">"keydown"</span>, { <span class="hljs-attr">keyCode</span>: <span class="hljs-number">27</span> });

  $(<span class="hljs-string">".innerScroll"</span>).click(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>)</span>{
    $(<span class="hljs-string">"body"</span>).trigger(esc);
  });
</code></pre>
<ul>
<li><p>Create a ‘Do when condition is true’ workflow and Put condition that this will trigger everytime when the popup is not visible.</p>
</li>
<li><p>On trigger of this workflow, set the ‘Run Javascript’ action with the following code.</p>
</li>
</ul>
<p>With this Scrolling on the page (background content of popup) is re-enabled, returning the page to its normal state.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//   This is every time the popup is not visible</span>

  $(<span class="hljs-string">"#experience"</span>).unwrap();

  $(<span class="hljs-string">"body"</span>).css(<span class="hljs-string">'overflow'</span>,<span class="hljs-string">'auto'</span>);
</code></pre>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-how-to-adjust-page-height-based-on-header-size-in-bubbleio-no-plugins">How to Adjust Page Height based on Header Size in bubble.io? <strong>(No Plugins)</strong></h2>
<p>Adjusting the page height based on the header size is important for creating a clean and professional layout. This ensures that your content dynamically adjusts to fit the available space, without being hidden behind or overlapping the header.</p>
<p>For example:</p>
<ul>
<li><p>On a page with a fixed header, you want the content area to fill the remaining screen space.</p>
</li>
<li><p>As the header's height might vary based on screen size or dynamic content, it’s important to account for these variations.</p>
</li>
</ul>
<p>The header can be a fixed or floating group, depending on your design.</p>
<ul>
<li><p>Add an <strong>HTML Element</strong> to your page where you want to insert the CSS.</p>
</li>
<li><p>Inside the HTML element, paste the following CSS code:</p>
</li>
</ul>
<pre><code class="lang-css">&lt;<span class="hljs-selector-tag">style</span>&gt;
<span class="hljs-selector-id">#pageheight</span> {
    <span class="hljs-attribute">min-height</span>: <span class="hljs-built_in">calc</span>(<span class="hljs-number">100vh</span> - <span class="hljs-number">60px</span>) <span class="hljs-meta">!important</span>; <span class="hljs-comment">/* Adjust '60px' with your header's height dynamically */</span>
}
&lt;/<span class="hljs-selector-tag">style</span>&gt;
</code></pre>
<p>Here <code>100vh</code> is the total viewport height (100% of the screen height).</p>
<p>set the ID <code>pageheight</code> of your Parent Groups other than the Header that needs to be adjusted based on the header.</p>
<div class="hn-embed-widget" id="akki47ak"></div><p> </p>
<h2 id="heading-how-to-create-full-height-sections-in-bubbleio-for-a-responsive-layout-no-plugins">How to Create Full-Height Sections in Bubble.io for a Responsive Layout? <strong>(No Plugins)</strong></h2>
<p>Creating sections that fill the entire screen height is a common requirement, especially for landing pages, hero sections, or single-page layouts. In Bubble.io, you might want to ensure that certain elements always take up at least the full height of the viewport, no matter how much content they contain.</p>
<ol>
<li><p>In the Bubble.io editor, go to the page where your section is located.</p>
</li>
<li><p>Drag and drop an <strong>HTML Element</strong> onto your page.</p>
</li>
<li><p>In the HTML Element, insert the following CSS code:</p>
</li>
</ol>
<pre><code class="lang-css">&lt;<span class="hljs-selector-tag">style</span>&gt;
<span class="hljs-selector-id">#viewport</span> {
    <span class="hljs-attribute">min-height</span>: <span class="hljs-number">100vh</span> <span class="hljs-meta">!important</span>;
}
&lt;/<span class="hljs-selector-tag">style</span>&gt;
</code></pre>
<p>Here <code>100vh</code> is the total viewport height (100% of the screen height).</p>
<p>you need to apply the <code>viewport</code> ID to the group you want to stretch.</p>
<ol>
<li><p>Select the <strong>Group</strong> element (the one you want to be full height) in the Bubble.io editor.</p>
</li>
<li><p>In the <strong>Property Editor</strong> for the group, look for the <strong>ID Attribute</strong> field.</p>
</li>
<li><p>Set the ID to <code>viewport</code>.</p>
</li>
</ol>
<p>In conclusion, mastering customization in Bubble.io can significantly enhance the user experience and interface of your applications. By leveraging custom code for scrollbars, uploaders, popups, and responsive layouts, you can create a more polished and professional look without relying heavily on plugins. These techniques not only improve the aesthetic appeal but also ensure functionality across various devices and screen sizes. As you continue to explore and implement these strategies, you'll find that Bubble.io offers a flexible platform for bringing your app design visions to life.</p>
]]></content:encoded></item><item><title><![CDATA[How to add custom fonts in bubble.io?]]></title><description><![CDATA[Why use Custom Fonts?
Typography plays a crucial role in application development, influencing an app's user interface and user experience.
While bubble.io provides many font options, sometimes your project needs a specific style not found in their li...]]></description><link>https://anishgandhi.com/how-to-add-custom-fonts-in-bubbleio</link><guid isPermaLink="true">https://anishgandhi.com/how-to-add-custom-fonts-in-bubbleio</guid><category><![CDATA[Font integration]]></category><category><![CDATA[custom fonts]]></category><category><![CDATA[bubble.io]]></category><category><![CDATA[bubble]]></category><category><![CDATA[typography]]></category><category><![CDATA[Web Design]]></category><category><![CDATA[No Code]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Thu, 07 Nov 2024 14:38:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1730867434747/61437622-30f0-4161-b635-47414ecd295d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-why-use-custom-fonts">Why use Custom Fonts?</h3>
<p>Typography plays a crucial role in application development, influencing an app's user interface and user experience.</p>
<p>While bubble.io provides many font options, sometimes your project needs a specific style not found in their library. Custom fonts allow you to stay on-brand with unique typography, create a visually distinct experience, and meet client specifications if you're working on a branded project.</p>
<p>Here is the step-by-step guide on how to add custom fonts:</p>
<h3 id="heading-step-1-download-the-font-file">Step 1: Download the font file</h3>
<p>For this article, I am going to use a custom font named ‘<a target="_blank" href="https://fontlibrary.org/en/font/taylor-sans">Taylor Sans</a>’ which is not part of the bubble.io editor. It has an ‘<a target="_blank" href="https://openfontlicense.org/">OFL (SIL Open Font License</a>’ so I can use it here in this blog freely.</p>
<p>I have downloaded this font from <a target="_blank" href="https://fontlibrary.org/en/font/taylor-sans">here</a>. This font family has 6 font files with different font weights. Here are the 6 font file names:</p>
<ul>
<li><p>TaylorSans-Bold - 700</p>
</li>
<li><p>TaylorSans-ExtraBold - 800</p>
</li>
<li><p>TaylorSans-Light - 300</p>
</li>
<li><p>TaylorSans-Regular - 400</p>
</li>
<li><p>TaylorSans-SemiBold - 600</p>
</li>
<li><p>TaylorSans-Thin - 100</p>
<p>  For this font family, you need to set up each font file independently. Here in this article, I will upload only one file ‘TaylorSans-Bold.ttf’. You can replicate the same for other files.</p>
</li>
</ul>
<h3 id="heading-step-2-upload-the-font-file">Step 2: Upload the font file</h3>
<p>All the font files will have the ‘.ttf’ extension so I will call it a ttf font file. Now open the bubble editor and place the file editor element on the dummy/test page.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730901745195/a3d483c0-4709-42d4-bf8d-576d698b504e.png" alt="A user interface showing a file uploader setup in a design editor. The sidebar features options for customizing the &quot;FileUploader Font&quot; element, including a placeholder text, upload button, and file settings." class="image--center mx-auto" /></p>
<p>Upload the ‘TaylorSans-Bold.ttf‘ file as a static file and copy the link generated in the dynamic link section once the file is uploaded.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730902458763/ba9c72ae-dfe5-4c24-9799-c6bd707baf65.png" alt="Screenshot of a UI Builder showing the process of uploading a font file named &quot;TaylorSans-Bold.ttf&quot;. The image includes the file uploader component, its properties panel, and instructions to upload files and save the file link." class="image--center mx-auto" /></p>
<h3 id="heading-step-3-create-a-css-script-i-have-mentioned-below-on-notepad">Step 3: Create a CSS script I have mentioned below on Notepad</h3>
<p>Copy the CSS I mentioned below and paste it on Notepad. I am using a<a target="_blank" href="https://onlinenotepad.org/notepad">n online notepad</a> for this article.</p>
<pre><code class="lang-css"><span class="hljs-keyword">@font-face</span> {
<span class="hljs-attribute">font-family</span>: <span class="hljs-string">'Font-title'</span>;
<span class="hljs-attribute">src</span>: <span class="hljs-built_in">url</span>(<span class="hljs-string">'file-upoader-url'</span>);
}
</code></pre>
<p>After pasting the CSS script on Notepad, change the Font-tile (In this case, it will be ‘TaylorSans-Bold’) and use the saved URL from step 2 which you got after uploading the .ttfl file</p>
<p>In the URL, add ‘https:’ before // and the CSS script in Notepad will look like the image provided below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730988220802/ba37b0c8-1926-4972-bc06-f5800af2523a.png" alt="A code snippet in an online notepad demonstrates a CSS  rule setting the font family to 'TaylorSans-Bold' with a URL source. Arrows indicate &quot;Font-Title&quot; and &quot;Font-URL.&quot;" class="image--center mx-auto" /></p>
<h3 id="heading-step-5-download-the-css-font-file">Step 5: Download the CSS font file</h3>
<p>From Notepad, save this file and make sure to give the file name the same as font-title and give it a .css extension at the end.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730988371669/34dd20a2-e885-478c-9104-d061e315324a.png" alt="A screenshot of an online notepad showing CSS code for a custom font named &quot;TaylorSans-Bold&quot;. There's a &quot;Save As&quot; dialog open, with the filename set as &quot;TaylorSans-Bold.css&quot;." class="image--center mx-auto" /></p>
<p>When you save it, it will be saved in the device as a ‘TaylorSans-Bold.css.txt’ file. Rename this file in your device as ‘TaylorSans-Bold.css’. Remove .txt from the file name.</p>
<h3 id="heading-step-6-upload-the-css-font-file">Step 6: Upload the CSS font file</h3>
<p>Now let’s come to the editor again and upload this .css file on file-uploader. Once uploaded, save the URL of css file as well.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730988933518/135aef3a-756d-49ca-83ac-c58907be6af5.png" alt="Screenshot showing a file upload interface for a CSS file named &quot;TaylorSans-Bold.css&quot; with instructions to upload the file and save its URL. The interface includes settings for appearance, layout, and conditions." class="image--center mx-auto" /></p>
<h3 id="heading-step-7-add-css-path-in-the-bubbleio">Step 7: Add CSS Path in the bubble.io</h3>
<p>Go to Settings → General → Custom fonts</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730989028914/1f1af166-65cb-4c90-8273-d932b2ff23dd.png" alt="A web interface for customizing fonts and importing design files from Figma. Includes input fields for font name, CSS file path, and Figma API key, with guidance on uploading font files and design imports." class="image--center mx-auto" /></p>
<p>Here make sure that the Font name value is the same as the font title, In our case it will be ‘TaylorSans-Bold’. Add CSS file path as URL saved in step 6 and add ‘https:’ before ‘//’ so it will look like the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730989331353/193c9244-4803-4a16-a3dd-fb508d193073.png" alt="A screenshot of a settings page for configuring custom fonts and design import in an app builder. It shows a tab labeled &quot;General,&quot; options for adding custom fonts with fields for font name and CSS file path, and instructions for importing design files from Figma. Red arrows point to specific sections." class="image--center mx-auto" /></p>
<p>Now click on Add Font and give it some time to load. It will look like the image below after added.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730989431893/7765e612-999f-4a17-aa71-4a4a974df082.png" alt="Screenshot of a web interface for adding custom fonts. Shows an example with the font &quot;TaylorSans-Bold&quot; and a CSS file path, with options for adding the font and a note about font libraries." class="image--center mx-auto" /></p>
<h3 id="heading-step-8-test-the-font-in-the-application-bubbleio-editor">Step 8: Test the font in the application bubble.io editor</h3>
<p>Add a text element in the editor and check the font where you will find ‘TaylorSans-Bold’ as an option for the text element.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730989660543/62b7ffc2-4e97-4af2-8d9f-72ed510b1830.png" alt="UI interface showing the selection of &quot;TaylorSans-Bold&quot; font from a dropdown menu in a design software. The page is labeled &quot;custom-fonts&quot; and the text element is named &quot;Text Taylor Sans - BOLD.&quot;" class="image--center mx-auto" /></p>
<p>Follow the same steps for all the files of font-family.</p>
<p>This is how you add custom fonts with just a URL link and a few settings, you can elevate your app’s design to a whole new level. It’s fun, right? 😉</p>
]]></content:encoded></item><item><title><![CDATA[Bubble.io Tips - 3]]></title><description><![CDATA[1. Privacy Rules Comparing One thing's Data with Another thing's Data
Let me explain the problem here:

In Bubble.io, a privacy rule like "This thing's salesforce is the current user's salesforce" can be risky if the "salesforce" field is empty for b...]]></description><link>https://anishgandhi.com/bubbleio-tips-3</link><guid isPermaLink="true">https://anishgandhi.com/bubbleio-tips-3</guid><category><![CDATA[bubble.io]]></category><category><![CDATA[bubble]]></category><category><![CDATA[Bubble Developers]]></category><category><![CDATA[No Code]]></category><category><![CDATA[nocode]]></category><category><![CDATA[no code development]]></category><category><![CDATA[APIs]]></category><category><![CDATA[api security]]></category><category><![CDATA[api security best practices]]></category><category><![CDATA[Bubble.io API security]]></category><category><![CDATA[Bubble.io API connector]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Tue, 22 Oct 2024 05:32:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729523181926/df1683cb-4e9c-4d5a-acde-8275292d2366.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-1-privacy-rules-comparing-one-things-data-with-another-things-data">1. Privacy Rules Comparing One thing's Data with Another thing's Data</h3>
<p>Let me <strong>explain the problem here</strong>:</p>
<ul>
<li>In Bubble.io, a privacy rule like "This thing's salesforce is the current user's salesforce" can be risky if the "salesforce" field is empty for both the item and the user. When both are empty, the condition is seen as true, allowing access to data that should be private. This can accidentally expose data (like access_token, refresh_token, etc.) for the entire data table because the rule doesn't differentiate between matching specific values and matching due to being empty.</li>
</ul>
<p>Here's <strong>how to fix it</strong>:</p>
<ul>
<li>Improve the privacy rule by adding another condition: "This thing's salesforce is the current user's salesforce AND the current user's salesforce is not empty." or "This thing's salesforce is another thing's salesforce AND another thing's salesforce is not empty."</li>
</ul>
<p><strong>Why does this solve the problem?</strong></p>
<ul>
<li>By adding "current user's salesforce is not empty," the rule only allows matches when both fields are filled, stopping empty fields from matching by mistake.</li>
</ul>
<p>This change s<strong>tops data leaks</strong> by making sure <strong>only users with a valid, non-empty entry can see the data</strong>, <strong>keeping privacy intact</strong>.</p>
<h3 id="heading-2initialized-api-calls-have-unnecessary-data">2.Initialized API calls have unnecessary data</h3>
<p>Plenty of times, I am unaware of which data from API responses I will need so I initialize the call and save all the responses. The time I am clear which data I need and which I don’t do the following in this special case.</p>
<p>In the image below, you can see I have successfully initialized the API Call and it is showing me all the data I am getting in my API call like access_token, signature, scope, instance_url, id, token_type, and issued_at.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729512839529/ec106d2e-2ec2-4639-bcb4-1467d6e2d557.png" alt="A screenshot of a Salesforce Token configuration window in Bubble, showing fields like access_token, signature, scope, instance_url, id, token_type, and issued_at with dropdown options set to &quot;text&quot;. A &quot;Save&quot; button is at the bottom." class="image--center mx-auto" /></p>
<p>But from this, I need only access_token. So in this successful API call, I can tell bubble to only accept access_token. It doesn’t make sense to receive all data when you need only a few So ignore the fields you don’t need as shown in the image below. <strong>The basic idea is to only have the data that the app needs to function properly.</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729513550005/3d9dae6a-1509-4e9b-b95f-36085ea97828.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-3-same-authentication-for-every-api-call">3. Same authentication for every API Call</h3>
<p>If the following conditions are met,</p>
<ul>
<li><p>There are <strong>multiple API Calls in a single API</strong> in a bubble.io API Connector</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729511004140/db897447-e80f-4eba-a2a1-ce4bb13780dc.png" alt="A user interface for configuring an API named &quot;Salesforce,&quot; with options for adding shared headers and parameters, and a dropdown for authentication settings. Various API parameter names are listed with options to move or expand them. The background is a gradient of purple and pink." class="image--center mx-auto" /></p>
</li>
<li><p>The <strong>same Private key/authentication token</strong> is being used for all the calls separately in their respective API Calls</p>
</li>
<li><p>The <strong>Private key/authentication token is static</strong></p>
</li>
</ul>
<p>Then Use the Private key in the Header and provide the same private key for all the headers in a single place.<br />If there are static parameters (header parameters/body parameters) common for all API calls you can define them in <strong>Shared headers for all calls</strong> and <strong>Shared parameters for all calls</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729512187954/2f1c086b-98e2-4b6b-ba42-22f3b6b3b919.png" alt="A screenshot of an API configuration interface with a pink background. It includes fields for API name, key name, authentication options, and buttons for adding shared headers and parameters. A dropdown menu for authentication methods is visible." class="image--center mx-auto" /></p>
<h3 id="heading-4-dont-use-http-endpoints">4. Don’t use HTTP Endpoints</h3>
<p>Many times, I discovered that API initialization showed an error about missing SSL, so I changed the endpoint from HTTPS to HTTP, and it worked. This is a warning sign.</p>
<p>Always make sure your API calls use <strong>HTTPS</strong> to protect your data from being intercepted by unauthorized parties. Check that the API base URL starts with <code>https://</code> in the API connector settings. Doing this ensures your application only interacts with APIs that support secure connections, which is important for keeping the data private and intact. Additionally, using HTTPS helps prevent security risks that could occur from using insecure HTTP connections, protecting both your application and its users.</p>
<h3 id="heading-5use-the-principle-of-least-privilege-polp">5.Use the principle of least privilege (PoLP)</h3>
<p>Definition: The principle of least privilege (PoLP) is an information security concept that states <strong>users and applications should only have access to the data and operations necessary to perform their tasks.</strong></p>
<p>In which areas this principle is applicable when working with APIs in bubble.io?</p>
<ul>
<li><p>Limit the Scope of API Access: Only add the permissions you need right now. Add more later if necessary.</p>
</li>
<li><p>Limit API Key Permissions: For example, in Stripe, create restricted keys that can only do specific tasks, like creating charges but not processing refunds.</p>
</li>
<li><p>Minimize Data in the API Request Body: See Tip 2 for more details.</p>
</li>
<li><p>Role-Based API Workflows: Allow only admin roles to perform sensitive API tasks, like managing users or handling financial transactions. Restrict basic users or customers to actions that only retrieve information, such as viewing data without updating or deleting it. Use conditional logic in Bubble to stop unauthorized roles from triggering certain API workflows.</p>
</li>
<li><p>Limit Developer Access: Only allow Live branch data access to certain developers who need it.</p>
</li>
</ul>
<p>Learn the best practices for Bubble.io API Security <a target="_blank" href="https://anishgandhi.com/bubble-security-api-best-practices">here</a> &amp; Don’t forget to sponsor me a cup of coffee 😉</p>
]]></content:encoded></item><item><title><![CDATA[Bubble.io API Security Best Practices]]></title><description><![CDATA[What are the risks of not making secure api calls with bubble.io?

Exposing sensitive information, such as passwords and tokens, because of unencrypted data transfers and visible API keys in logs.

Unauthorized access and token theft.

Risk service d...]]></description><link>https://anishgandhi.com/bubble-security-api-best-practices</link><guid isPermaLink="true">https://anishgandhi.com/bubble-security-api-best-practices</guid><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Mon, 21 Oct 2024 05:42:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729495546012/b2e06894-f7d6-47fa-ae73-8d5f421c7b29.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-what-are-the-risks-of-not-making-secure-api-calls-with-bubbleio">What are the risks of not making secure api calls with bubble.io?</h2>
<ul>
<li><p>Exposing sensitive information, such as passwords and tokens, because of unencrypted data transfers and visible API keys in logs.</p>
</li>
<li><p>Unauthorized access and token theft.</p>
</li>
<li><p>Risk service disruptions, account takeovers, and potential compliance issues.</p>
</li>
<li><p>It opens the door to injection attacks and data leaks.</p>
</li>
</ul>
<hr />
<p>In the <a target="_blank" href="https://anishgandhi.com/how-to-implement-oauth-2-in-bubble">previous article</a>, I have shown how to set API calls and OAuth in bubble.io using the bubble api connector.<br />A successful API call might look like in the images shown below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729447531476/9ed1e732-1601-40c8-b6ac-d23951a97930.png" alt="A user interface screen for configuring an API call to refresh a token. It includes fields for URL, method (POST), headers, body type, and parameters. There are options for capturing response headers and parameters for the body." class="image--center mx-auto" /></p>
<p>OR</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729447688251/4d09cc78-4f7f-430c-b92c-0075bda16896.png" alt="A screenshot showing API request configuration with parameters for a &quot;Refresh Token&quot; action. It includes fields for client ID, client secret, grant type, and refresh token. Options for headers and response settings are also visible." class="image--center mx-auto" /></p>
<p><strong>These API calls are not secure. Let me explain how.</strong></p>
<ul>
<li><strong>This API call, endpoint URL, and all its tokens (Client ID, Client Secret, Authentication token, refresh token, etc.) are visible on the developer console on each page load of the bubble.io application. It doesn't matter if I am using this API call or not. It will be visible to logged-out users (Unauthorised users) as well on any page load.</strong></li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729447847721/4439e4dd-2639-443a-ad3a-da4994c48790.png" alt="Screenshot of a browser developer console displaying network request details for an OAuth 2.0 token refresh, with parameters including client ID, client secret, and grant type." class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-best-practices-for-implementing-secure-api-calls">Best Practices for implementing secure API calls</h2>
<p>Here are some of the best practices for implementing secure API calls and token management:</p>
<h3 id="heading-step-1-conceal-api-url-endpoint">Step 1: <strong>Conceal API URL Endpoint</strong></h3>
<p>Now you must be <strong>wondering</strong> <strong>why should I hide my API URL Endpoint</strong>. Here is why:</p>
<ul>
<li>Exposing the endpoint could let malicious users access the API directly, understand my application's structure, and possibly exploit vulnerabilities. It also helps protect the privacy of my business logic and interactions with third-party services, maintaining both security and a competitive edge. Keeping the endpoint hidden adds an important layer of security to my application.</li>
</ul>
<p>After understanding why, <strong>let’s dive into how</strong>.</p>
<p>As you can see in the screenshot attached below, I have divided the endpoint URL: https://login.salesforce.com/services/oauth2/token into 2 parts and put them in square bracket [url]/[endpoint]</p>
<p>Then I have defined url=https://login.salesforce.com/services/oauth2 and endpoint=token</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729448735223/397bc1de-5668-4e2f-87c1-be39bdc99063.png" alt="A screenshot of a software interface showing a form titled &quot;Refresh Token.&quot; It features a POST method with fields for URL and endpoint values, and a Content-Type header set to &quot;application/x-www-form-urlencoded.&quot;" class="image--center mx-auto" /></p>
<h3 id="heading-step-2-safeguard-static-parameters">Step 2: <strong>Safeguard Static Parameters</strong></h3>
<p>When making an API call, certain parameters remain constant, regardless of whether the call is a Data call or an Action call. These are known as static parameters. It is crucial to ensure that all these static parameters are kept private. By doing so, you protect sensitive information from being exposed or accessed by unauthorized users. This will prevent them from being visible on the client side through the developer console. Checkout the screenshot below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729450121433/ab1d6faf-ca4f-4e11-9661-55b108aa4ac4.png" alt="A screenshot of a web application interface for configuring a &quot;Refresh Token&quot; API call. The configuration includes URL and endpoint fields, headers, and parameters like client_id, client_secret, grant_type, and refresh_token. Red arrows highlight fields with static values marked as private. A note indicates that all parameters, except the refresh token, are static and should be private." class="image--center mx-auto" /></p>
<p><strong>On the Bubble.io app page load, the browser downloads all api calls on the user’s device, and if you don’t keep the static parameters like client_id, and client_secret private; anyone can access these parameter values on their device using the console.</strong></p>
<h3 id="heading-step-3-manage-dynamic-parameters-securely">Step 3: <strong>Manage Dynamic Parameters Securely</strong></h3>
<p><strong>What are the dynamic parameters in API calls?</strong></p>
<ul>
<li>When making an API call, some parameters may change based on specific inputs or conditions at the time of the call. These are called dynamic parameters. Unlike static parameters, which stay the same whether it's a Data call or an Action call, dynamic parameters are flexible. They can be updated with values that are generated or provided during each API request. This flexibility allows the API call to adapt to different situations or user inputs as needed.</li>
</ul>
<p>Let’s understand how to make them secure.</p>
<p>Once I initialize the API call, I will remove all the parameters from the API connector that are not private. On the Above image of step 2, you can see that refresh_token is not private and because of that, it will still be visible on the developer console on every page load. Check out the below image:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729489283228/cbcc2d5a-e8dc-4d67-96be-6780b4c71590.png" alt class="image--center mx-auto" /></p>
<p>This is still risky so remove it from here and because it is a dynamic parameter that varies from user to user, I will store all the dynamic parameters in the database. Check the screenshot below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729487481539/1cc97dcc-b7b8-4db3-be1a-e11ea9507f4b.png" alt="A screenshot of an API configuration interface. It includes fields for setting up a POST request with URL parameters, headers, and JSON body parameters like , , , and . An annotation points to a dynamic parameter with the note: &quot;Dynamic parameter removed from API connector.&quot;" class="image--center mx-auto" /></p>
<h3 id="heading-step-4-implement-privacy-rules-for-dynamic-data">Step 4: <strong>Implement Privacy Rules for Dynamic Data</strong></h3>
<p>As shown in the screenshot above of step 3, the refresh token is a dynamic parameter that I did not make private. It is stored in the database as a field in a certain data type.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729450641571/403f146e-fa11-4dfb-ba9a-eaa333682ecb.png" alt="A user interface showing a data type named &quot;Salesforce&quot; with fields such as &quot;access_token,&quot; &quot;expired_by,&quot; and &quot;refresh_token.&quot; The &quot;Salesforce&quot; type is publicly visible, and fields are categorized by type, like text and date. A form for creating a new field is also visible." class="image--center mx-auto" /></p>
<p>Without privacy rules, data is basically open to anyone using the app. Any user can possibly access the database data for that specific type. This includes data from things like repeating groups, text elements, and API requests. Any workflows that create, change, or show data that can lead to unauthorized changes or exposure of data.</p>
<p>So here I have defined privacy rules that will accomplish the following:</p>
<ul>
<li><p>Do not show the refresh token, access token, and access token expiry date to any users who are logged out.</p>
</li>
<li><p>When a user is logged in, the application will allow access only to their own refresh token, access token, and access token expiry date. Users will not be able to see anyone else's refresh token, access token, or access token expiry date.</p>
</li>
</ul>
<p>Checkout the image below to check the privacy rules setup:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729451628298/67ee9219-b5d5-4c67-a158-d224c062939d.png" alt="A screenshot of privacy settings in a bubble.io interface. The &quot;Salesforce&quot; data type is selected, showing detailed rules for logged-in users to view their own data and permissions for other users. Options include fields for access token, created by, and others, with checkboxes for permissions." class="image--center mx-auto" /></p>
<h3 id="heading-step-5-replace-initialised-api-call-response-with-placeholders">Step 5: Replace Initialised API Call Response with placeholders</h3>
<p>Let’s understand how bubble.io works:</p>
<ul>
<li><p>When I set up an API call in Bubble.io, the API Connector plugin automatically makes a test call to understand how the API response is structured. This process creates a response schema - a template that represents the format and structure of the data the API returns. This schema helps Bubble understand what kind of data to expect when the API call is made in real-time.</p>
</li>
<li><p>After the initial test call, the response schema is stored in my app's configuration, specifically in a file called <strong>app.json</strong>. This file is part of my app's settings and can be accessed by others if it's not secured properly. Therefore, if the test API call includes sensitive information - like access tokens, API keys, user data, or app IDs - this information will be saved as part of the schema.</p>
</li>
</ul>
<p><strong>Issue: If I don’t clean up these sensitive details before deploying my app, they might become visible to anyone who can access the app.json file or inspect the API response structure.</strong></p>
<p><strong>Solution:</strong></p>
<ul>
<li><p>As you can see in the image shown below, my initialized api call contains actual sensitive information that will be visible on the developer console and if it is leaked, my account, my credits, my data, etc can be misused.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729562169404/f91e853b-0aff-4cc9-b6be-9607baaf15b7.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Now, when you save it, you will know the exact type (like text, number, yes/no, file, image) of the data each field contains. check the image below for more clarity.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729562435697/df9e0499-5f62-42db-bb00-23374164c505.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>The reason for the bubble.io to save it is just to understand data structure whenever an API call is made that means, in manual response if there is ‘access_token’ and the type of field is text then I don’t need actual access_token but some arbitrary text which will let api response to identify as text.</p>
</li>
<li><p>So I will replace the existing original data with a placeholder for the same type of data</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729563013272/0d8bc38c-48a5-4246-9586-d44419972166.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
<p>Following these steps, your app's response schema will contain placeholders instead of sensitive data. When you deploy your application, any potential exposure of sensitive information will be avoided. This practice enhances the security of your app, especially when dealing with data like access tokens, API keys, and user information.</p>
<h3 id="heading-step-6-limit-developers-access-to-dynamic-tokens">Step 6: Limit Developers' Access to Dynamic Tokens</h3>
<p>Now, developers working with applications and having access to the Bubble.io app editor can also access the data. However, not every developer needs to see this information. Bubble.io offers a way to restrict developer access to data as well.</p>
<p>Go to Setting → Collaboration and restrict the data access for developers who don’t need it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729452348466/b3659752-a811-4f2c-b449-ed3db6e73ab0.png" alt="Screenshot of a user permissions settings page. It shows options for user roles and data access levels, with drop-down menus offering permissions such as &quot;View and edit.&quot; Two users' email addresses are visible, alongside options to invite or transfer users. Navigation tabs are on the left side." class="image--center mx-auto" /></p>
<p>For Data, there will be 4 options explained below:</p>
<ul>
<li><p>No permission - (cannot see or edit any database data in Development or Live)</p>
</li>
<li><p>View only - (can view data but cannot change it in the database editor)</p>
</li>
<li><p>View and run as - (can view data and use the <em>run as</em> feature)</p>
</li>
<li><p>View and edit - (can view and freely edit data)</p>
</li>
</ul>
<hr />
<h2 id="heading-how-did-i-secure-api-calls-amp-tokens-till-now">How did I secure API Calls &amp; tokens till now?</h2>
<ul>
<li><p><strong>With Step 1</strong>, I ensure that no user can identify the API calls the app is making, regardless of their login status.</p>
</li>
<li><p><strong>With Step 2</strong>, static parameters like the client ID and client secret are completely inaccessible in the developer console for all users, whether logged in or out.</p>
</li>
<li><p><strong>With Step 3</strong>, dynamic parameters are hidden for all users (authorized/unauthorized) in the developer console of the browser on page load.</p>
</li>
<li><p><strong>With Step 4</strong>, dynamic parameters are hidden from logged-out users, and logged-in users are restricted from accessing tokens of other users.</p>
</li>
<li><p><strong>With Step 5</strong>, Anyone (authorized/unauthorized) won’t be able to find actual data in bubble.io’s saved API response because I have replaced it with placeholders.</p>
</li>
<li><p><strong>With Step 6</strong>, developers who don't need to view or edit data are denied access.</p>
</li>
</ul>
<p>This is how I recommend securing API calls and tokens from any user, logged-out users, logged-in users, and unauthorized developers.</p>
<h2 id="heading-special-cases-amp-your-api-practices"><strong>Special Cases &amp; Your API Practices</strong></h2>
<p>Tell me, how you are setting up your APIs in bubble.io currently? &amp; buy me a coffee 😉. Also If you would like to learn about some special cases tips for API Security, click <a target="_blank" href="https://anishgandhi.com/bubbleio-tips-3">here</a>.</p>
]]></content:encoded></item><item><title><![CDATA[How can you effectively prevent deleted pages from consuming workload units (WU)?]]></title><description><![CDATA[Seriously! Do deleted pages consume workload Units?
Yes! Deleted pages do consumer workload units. To understand why they consume it and how to effectively prevent them, Let’s understand the type of deleted pages.
2 categories of deleted pages in Bub...]]></description><link>https://anishgandhi.com/how-can-you-effectively-prevent-deleted-pages-from-consuming-workload-units-wu</link><guid isPermaLink="true">https://anishgandhi.com/how-can-you-effectively-prevent-deleted-pages-from-consuming-workload-units-wu</guid><category><![CDATA[optimization]]></category><category><![CDATA[bubble.io]]></category><category><![CDATA[bubble]]></category><category><![CDATA[Workload units]]></category><category><![CDATA[performance]]></category><category><![CDATA[Performance Optimization]]></category><category><![CDATA[nocode]]></category><category><![CDATA[No Code]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Mon, 23 Sep 2024 04:30:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726926295106/e7f78436-ea12-4602-9cbd-af2487308eb7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-seriously-do-deleted-pages-consume-workload-units">Seriously! Do deleted pages consume workload Units?</h3>
<p>Yes! Deleted pages do consumer workload units. To understand why they consume it and how to effectively prevent them, Let’s understand the type of deleted pages.</p>
<h3 id="heading-2-categories-of-deleted-pages-in-bubbleio">2 categories of deleted pages in Bubble.io</h3>
<p>There are 2 kinds of deleted pages in the bubble.</p>
<ol>
<li><p>Pages you created and then deleted from the editor manually.</p>
</li>
<li><p>Deleted but not created by you are default pages automatically set up by Bubble, such as the index and 404 pages.</p>
</li>
</ol>
<h3 id="heading-why-do-these-deleted-pages-consume-workload-units">Why do these deleted pages consume workload units?</h3>
<ul>
<li><p>Manually deleted pages by you can consume workload units if they had workflows attached to them that were not properly removed before deletion. This can happen because the system continues recognizing the workflows until they are explicitly removed or the associated elements are deleted. If you have deleted pages and notice unexpected workload consumption, it may be due to these lingering workflows.</p>
</li>
<li><p>Default pages set up by bubble and not manually created by you are necessary. for example, The 404 page specifically displays an error when someone attempts to access a non-existent page.</p>
<p>  So, if you come across deleted pages you didn't create, they are likely the default ones by bubble and when they load, they consume workload units.</p>
</li>
</ul>
<h3 id="heading-how-to-stop-these-deleted-pages-from-consuming-workload-units-wu">How to stop these deleted pages from consuming workload units (WU)?</h3>
<ul>
<li><p><strong>To optimize your Bubble application when dealing with default pages that have been deleted but not created by you</strong> or <strong>pages that were created and deleted by you</strong>, consider the following steps:</p>
<ol>
<li><p>Make sure all workflows are correctly deleted before removing pages or elements in the future. It's crucial to understand that once these deleted elements are no longer in the system, they will no longer consume workload units.</p>
</li>
<li><p>Upgrade to the latest Bubble version to benefit from recent performance improvements.</p>
</li>
<li><p>Uninstall any plugins that are not actively used in your application.</p>
</li>
<li><p>Remove broken links from internal application navigation which leads to the loading of 404 pages.</p>
</li>
<li><p>Utilize the 'clean app/app optimization' tool in Settings &gt; General to help streamline your app's performance.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726948223259/9bd3a98f-ae3f-40ac-88ca-451b62583d4e.png" alt="Optimise bubble application" class="image--center mx-auto" /></p>
</li>
</ol>
</li>
</ul>
<p>Implementing this will not just only reduce unnecessary workload unit consumption but will improve the performance of your application as well.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727022030346/e898f14f-8b9d-4ef3-9bff-74983aceff33.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-help-me"><strong>Help me!</strong></h3>
<p>If you enjoyed this post and found it helpful, Kindly consider supporting my work by buying me a coffee! Your support helps me create more valuable content and continue sharing useful resources. Thank you!</p>
]]></content:encoded></item><item><title><![CDATA[The learning curve of the bubble (no-code) developer explained using the dunning-Kruger effect]]></title><description><![CDATA[What is the Dunning-Kruger effect?
The Dunning–Kruger effect is a cognitive bias in which people with limited competence in a particular domain overestimate their abilities. It was first described by Justin Kruger and David Dunning in 1999.
It can af...]]></description><link>https://anishgandhi.com/the-learning-curve-of-the-bubble-no-code-developer-explained-using-the-dunning-kruger-effect</link><guid isPermaLink="true">https://anishgandhi.com/the-learning-curve-of-the-bubble-no-code-developer-explained-using-the-dunning-kruger-effect</guid><category><![CDATA[No Code]]></category><category><![CDATA[Low Code]]></category><category><![CDATA[bubble.io]]></category><category><![CDATA[bubble]]></category><category><![CDATA[Bubble Developers]]></category><category><![CDATA[Learning Journey]]></category><category><![CDATA[#LearningCurve]]></category><category><![CDATA[dunning-kruger-effect]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Sat, 21 Sep 2024 20:44:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726950044705/259c5f87-7926-4f34-9bf5-525d0ccd035a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-what-is-the-dunning-kruger-effect">What is the Dunning-Kruger effect?</h3>
<p>The <strong>Dunning–Kruger effect</strong> is a <a target="_blank" href="https://en.wikipedia.org/wiki/Cognitive_bias">cognitive bias</a> in which people with limited competence in a particular domain overestimate their abilities. It was first described by <a target="_blank" href="https://en.wikipedia.org/wiki/Justin_Kruger">Justin Kruger</a> and <a target="_blank" href="https://en.wikipedia.org/wiki/David_Dunning">David Dunning</a> in 1999.</p>
<p>It can affect anyone, but it's more likely to happen to people who lack knowledge or skills, are overconfident, or are poor performers. The Dunning-Kruger effect can also affect people who excel in a given area. These people may underestimate their abilities because they think the task is simple for everyone.</p>
<h3 id="heading-the-dunning-kruger-effect-in-no-code-development">The Dunning-Kruger Effect in No-Code Development</h3>
<p>At first, the no-code landscape feels like an exciting world of possibilities. With a few clicks and drag-and-drop functionalities, you can create a working prototype in no time. For beginners, this immediate success may lead to overconfidence. After all, if you’ve built a functioning app in days, how hard could the rest of it be?</p>
<p>Here’s where the Dunning-Kruger Effect comes into play. The initial ease of creating something tangible can lead to the belief that no-code programming is far simpler than it actually is. This false sense of mastery often leads to a harsh reality check when more complex tasks—like optimizing performance, integrating APIs, or solving bugs—require deeper knowledge and problem-solving skills.</p>
<h3 id="heading-the-peak-of-mount-stupid-for-no-code-developers">The “Peak of Mount Stupid” for No-Code Developers</h3>
<p>In the Dunning-Kruger Effect, beginners often reach what’s called the "Peak of Mount Stupid"—a phase where confidence is high, but actual knowledge is still limited. For no-code developers, this peak occurs after they’ve created a few basic applications but have yet to encounter the more intricate challenges that come with real-world projects.</p>
<p>At this stage, many no-code programmers might believe they’ve mastered the platform. They might take on more complex projects without realizing the potential pitfalls ahead. This overconfidence can lead to errors, miscommunication with clients, and even stalled projects when they encounter problems they aren’t equipped to solve.</p>
<h3 id="heading-the-valley-of-despair-for-no-code-developers">The "Valley of Despair" for No-Code Developers</h3>
<p>Inevitably, the beginner no-code programmer will encounter challenges they aren’t prepared for, such as a complex client request or unforeseen technical roadblocks. This marks the entry into the <strong>"Valley of Despair,"</strong> where the realization of how much they don’t know hits hard. It’s a humbling experience that often leads to frustration, self-doubt, and, for some, giving up entirely.</p>
<p>However, this valley is a necessary part of the learning process. It’s where true growth begins, as no-code programmers realize the depth and complexity of the field and start to seek more knowledge.</p>
<h3 id="heading-the-slope-of-enlightenment-for-no-code-developers">The "Slope of Enlightenment" for No-Code Developers</h3>
<p>After weathering the Valley of Despair, the dedicated no-code programmer starts climbing what’s called the <strong>"Slope of Enlightenment."</strong> This is the phase where they begin to truly understand the nuances of no-code development, not just at a surface level but in a deeper, more strategic sense.</p>
<ul>
<li><p><strong>Developing Real Expertise</strong><br />  As programmers push through complex challenges, they start developing a more refined skill set. They begin to understand platform limitations, how to optimize workflows, and which plugins or APIs can save time and improve functionality. This stage is about building real competence, not just the illusion of mastery.</p>
</li>
<li><p><strong>Recognizing Patterns</strong><br />  At this stage, no-code programmers start recognizing patterns in the problems they solve. They can anticipate potential issues and have a toolkit of solutions ready for common problems. This marks the transition from reactive problem-solving to proactive development.</p>
</li>
<li><p><strong>Confident but Not Complacent</strong><br />  Confidence returns on the Slope of Enlightenment, but this time it’s rooted in real experience. The no-code developer has learned to balance confidence with humility, knowing there’s always more to learn but feeling equipped to take on most challenges.</p>
</li>
</ul>
<h3 id="heading-the-plateau-of-sustainability-for-no-code-developers">The "Plateau of Sustainability" for No-Code Developers</h3>
<p>Once no-code developers have honed their skills and built a solid foundation of experience, they reach the <strong>"Plateau of Sustainability."</strong> This stage represents the point at which they can consistently deliver high-quality projects, efficiently handle complex requirements, and troubleshoot effectively.</p>
<ul>
<li><p><strong>Building Reliable Systems</strong><br />  At the Plateau of Sustainability, no-code programmers are no longer struggling with the basics. They can now build robust, scalable systems that meet client needs. This stability allows them to take on more significant and more lucrative projects with confidence.</p>
</li>
<li><p><strong>Continual Improvement</strong><br />  The Plateau of Sustainability isn’t about stagnation. Instead, it’s about continuous improvement. Developers on this plateau are constantly fine-tuning their processes, learning new tools, and finding ways to deliver better results faster. While they’ve achieved mastery of the no-code platform, they never stop learning.</p>
</li>
<li><p><strong>Mentorship and Community Engagement</strong><br />  Developers at this stage often give back to the community. They become mentors, sharing their knowledge with beginners who are still navigating the early phases of the Dunning-Kruger Effect. This engagement not only helps others but also reinforces their understanding.</p>
</li>
</ul>
<h3 id="heading-overcoming-the-dunning-kruger-effect-as-a-no-code-programmer">Overcoming the Dunning-Kruger Effect as a No-Code Programmer</h3>
<p>To successfully navigate the stages of the Dunning-Kruger Effect, the Slope of Enlightenment, and the Plateau of Sustainability, no-code programmers should follow these strategies:</p>
<ul>
<li><p><strong>Acknowledge What You Don’t Know</strong><br />  Stay humble in the early stages of no-code development. Recognize that what feels easy at first is only the beginning, and there is always more to learn.</p>
</li>
<li><p><strong>Seek Mentorship and Feedback</strong><br />  Surround yourself with experienced developers who can provide constructive feedback. This will help you identify your blind spots and accelerate your learning curve.</p>
</li>
<li><p><strong>Embrace Challenges and Failure</strong><br />  Don’t shy away from difficult projects. Embrace the mistakes and frustrations that come with the Valley of Despair. They are stepping stones to greater understanding.</p>
</li>
<li><p><strong>Invest in Continuous Learning</strong><br />  Mastery is not a final destination; it’s an ongoing process. Stay curious, take courses, and experiment with new no-code tools and techniques to keep growing.</p>
</li>
<li><p><strong>Contribute to the Community</strong><br />  Once you reach the Plateau of Sustainability, consider becoming a mentor or contributor to the no-code community. Teaching others will reinforce your expertise.</p>
</li>
</ul>
<p>Hope you will be more self-aware after reading this.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727022502300/1d684d7e-9192-43f9-9c8d-8a331ab36740.png" alt="Help me write more for you" class="image--center mx-auto" /></p>
<h3 id="heading-help-me"><strong>Help me!</strong></h3>
<p>If you enjoyed this post and found it helpful, Kindly consider supporting my work by buying me a coffee! Your support helps me create more valuable content and continue sharing useful resources. Thank you!</p>
]]></content:encoded></item><item><title><![CDATA[How to create Skeleton Loading in Bubble?]]></title><description><![CDATA[What is skeleton loading?
Skeleton loading is a placeholder component that mimics the structure of the application’s content while data is being loaded. Instead of showing a blank screen or a simple spinner, one can display a greyed-out or animated p...]]></description><link>https://anishgandhi.com/how-to-create-skeleton-loading-in-bubble</link><guid isPermaLink="true">https://anishgandhi.com/how-to-create-skeleton-loading-in-bubble</guid><category><![CDATA[skeleton loading]]></category><category><![CDATA[bubble.io]]></category><category><![CDATA[bubble]]></category><category><![CDATA[Bubble Developers]]></category><category><![CDATA[CSS]]></category><category><![CDATA[UI Design]]></category><category><![CDATA[No Code]]></category><category><![CDATA[nocode]]></category><category><![CDATA[Low Code]]></category><category><![CDATA[low code development]]></category><category><![CDATA[ low-code / no-code]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Sun, 15 Sep 2024 14:25:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726393275433/64d9c744-0a51-4af5-a1bc-e0a871a5af89.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-what-is-skeleton-loading">What is skeleton loading?</h3>
<p>Skeleton loading is a placeholder component that mimics the structure of the application’s content while data is being loaded. Instead of showing a blank screen or a simple spinner, one can display a greyed-out or animated placeholder resembling the content that will soon appear. This gives the impression that something is happening in the background and makes the wait feel shorter.</p>
<p>Skeleton loading has become a popular UX feature across applications, helping to improve user experience by indicating that content is being loaded while minimizing frustration caused by blank screens.</p>
<p>Here is how you can create one in bubble.io:</p>
<h3 id="heading-step-1-decide-the-place-for-skeleton-loading">Step 1: Decide the place for skeleton loading</h3>
<p>Generally, don’t use skeleton loading for the whole page but identify the components that are taking time in loading the data. In most cases, it is a repeating group loading data on page load.</p>
<h3 id="heading-step-2-prepare-the-final-output-that-will-be-seen-after-the-data-is-loaded">Step 2: Prepare the final output that will be seen after the data is loaded</h3>
<p>Here in this example, this is how the final Repeating Group will look like with data!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726394146554/157526f2-17a6-4b3a-ba7c-4f92575ba7d9.png" alt="Final output with data in Repeating Group" class="image--center mx-auto" /></p>
<p>Now let’s see this Repeating Group’s setup:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726394271203/482a9607-ab04-4ca4-8f4d-cdb3f3a493f7.png" alt="Repeating Group Main Property Setup" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726394409968/9f14d0b8-3fd8-4b49-abc7-e2d66416fd81.png" alt="Repeating Group Main layout Setup" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726394810016/511a848e-a18d-46b7-a70e-642e5a8bfca7.png" alt="Group inside output repeating group setup" class="image--center mx-auto" /></p>
<p>Once the output Repeating Group Setup with its component and its layout, it is time to go for the next step.</p>
<h3 id="heading-step-3-setup-skeleton-loading-repeating-group">Step 3: Setup Skeleton Loading Repeating Group</h3>
<p>Duplicate the output repeating group without its child groups and add the HTML element in place of Group main. Element tree will look like shown below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726395168304/be7f65d2-ad1d-4677-b2d9-5ea5e9452710.png" alt="Skeleton Loading Repeating Group element tree" class="image--center mx-auto" /></p>
<p>Now this Duplicate Repeating Group’s data source will be arbitrary text split by(,), and the data type will be text. Here is what it will look like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726395440438/95db3e60-db73-47ce-9b8c-5fb431fa21bb.png" alt="Data source of repeating group" class="image--center mx-auto" /></p>
<p>Now as per my output Repeating Group, I want to show one row only as a skeleton loading that is why I have used 4 times 1. If I wanted to show 2 rows, I would have used 1,1,1,1,1,1,1,1 as arbitrary text split by(,). This will vary as per your output repeating group setup.</p>
<p>Now, About the HTML element that we placed inside this skeleton Repeating Group or Duplicate Repeating Group. Here is the CSS I have used in my case:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726396077914/540999f1-22df-4735-8688-47b3a224ea95.png" alt="html/css image of skeleton loader" class="image--center mx-auto" /></p>
<p>Here is the CSS:</p>
<pre><code class="lang-css">&lt;<span class="hljs-selector-tag">div</span> <span class="hljs-selector-tag">class</span>="<span class="hljs-selector-tag">skeleton-item</span>"&gt;
    &lt;<span class="hljs-selector-tag">div</span> <span class="hljs-selector-tag">class</span>="<span class="hljs-selector-tag">skeleton-thumbnail</span>"&gt;&lt;/<span class="hljs-selector-tag">div</span>&gt;
    &lt;<span class="hljs-selector-tag">div</span> <span class="hljs-selector-tag">class</span>="<span class="hljs-selector-tag">skeleton-content</span>"&gt;
        &lt;<span class="hljs-selector-tag">div</span> <span class="hljs-selector-tag">class</span>="<span class="hljs-selector-tag">skeleton-throb</span>"&gt;&lt;/<span class="hljs-selector-tag">div</span>&gt;
        &lt;<span class="hljs-selector-tag">div</span> <span class="hljs-selector-tag">class</span>="<span class="hljs-selector-tag">skeleton-throb</span>"&gt;&lt;/<span class="hljs-selector-tag">div</span>&gt;
    &lt;/<span class="hljs-selector-tag">div</span>&gt;
&lt;/<span class="hljs-selector-tag">div</span>&gt;

&lt;<span class="hljs-selector-tag">style</span>&gt;
<span class="hljs-selector-class">.skeleton-item</span> {
    <span class="hljs-attribute">padding</span>: <span class="hljs-number">16px</span>;
    <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">8px</span>;
    <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
    <span class="hljs-attribute">height</span>: <span class="hljs-number">100%</span>;
    <span class="hljs-attribute">box-sizing</span>: border-box; <span class="hljs-comment">/* Ensure padding is included in the width/height calculation */</span>
    <span class="hljs-attribute">position</span>: absolute; <span class="hljs-comment">/* Position relative to the container */</span>
    <span class="hljs-attribute">top</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">left</span>: <span class="hljs-number">0</span>;
}

<span class="hljs-selector-class">.skeleton-thumbnail</span> {
    <span class="hljs-attribute">background-color</span>: <span class="hljs-number">#000000</span>;
    <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
    <span class="hljs-attribute">height</span>: <span class="hljs-number">60%</span>; <span class="hljs-comment">/* Allocate 60% of the skeleton-item's height */</span>
    <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">8px</span>;
    <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-number">8px</span>;
}

<span class="hljs-selector-class">.skeleton-content</span> {
    <span class="hljs-attribute">display</span>: flex;
    <span class="hljs-attribute">flex-direction</span>: column;
    <span class="hljs-attribute">gap</span>: <span class="hljs-number">6px</span>;
    <span class="hljs-attribute">height</span>: <span class="hljs-number">40%</span>; <span class="hljs-comment">/* Allocate the remaining 40% to the content */</span>
}

<span class="hljs-selector-class">.skeleton-throb</span> {
    <span class="hljs-attribute">background-color</span>: <span class="hljs-number">#15302d</span>;
    <span class="hljs-attribute">height</span>: <span class="hljs-number">10px</span>;
    <span class="hljs-attribute">width</span>: <span class="hljs-number">80%</span>;
    <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">4px</span>;
    <span class="hljs-attribute">animation</span>: throbbing <span class="hljs-number">1.5s</span> infinite ease-in-out;
}

<span class="hljs-keyword">@keyframes</span> throbbing {
    0% {
        <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    }
    50% {
        <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0.5</span>;
    }
    100% {
        <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    }
}
&lt;/<span class="hljs-selector-tag">style</span>&gt;
</code></pre>
<p>Now you can customise this as per your understanding of HTML and CSS or you can use Chat GPT to modify this as per your repeating group or container.</p>
<p>Basically in my case, I have put placeholders for the image element and 2 text elements</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726396545202/79fbbd3d-b149-4605-8f07-045c0a3ae584.png" alt="Look of Skeleton CSS in bubble editor" class="image--center mx-auto" /></p>
<h3 id="heading-step-4-hide-amp-show-condition-of-output-and-skeleton-loader-repeating-groups">Step 4: Hide &amp; Show condition of output and skeleton loader Repeating Groups</h3>
<ul>
<li><p>Both repeating groups should be ‘collapse when hidden’</p>
</li>
<li><p>Duplicate/Skeleton loading repeating group will be visible on the page load</p>
</li>
<li><p>Output Repeating Group will not be visible on the page load</p>
</li>
<li><p>Duplicate/Skeleton loading repeating group will not be visible when the Output Repeating Group’s data is loaded</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726397212807/b52ff022-2213-4890-a1b3-d65700b121c7.png" alt="skeleton repeating group condition" class="image--center mx-auto" /></p>
<ul>
<li><p>Output Repeating Group will not be visible when it is loading the data and will be loaded only when its data count is more than 0</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726397291996/1292bd59-e94f-444a-8ca4-31adbb9b6879.png" alt="Main repeating group condition" class="image--center mx-auto" /></p>
<p>  And here is how it will look in the end</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726397441668/91d40794-52f3-4891-8098-dfd366c158a7.gif" alt="Skeleton loading in repeating group of bubble" class="image--center mx-auto" /></p>
<h3 id="heading-step-5-best-practices-to-consider-for-using-skeleton-loading">Step 5: Best Practices to consider for using Skeleton loading</h3>
<ul>
<li><p><strong>Minimal Overhead</strong>: Keep your skeleton design simple to ensure it doesn’t impact performance. Avoid too many elements or complex animations.</p>
</li>
<li><p><strong>Mobile Optimization</strong>: Ensure the skeleton loading screen looks good on mobile devices by testing responsiveness.</p>
</li>
<li><p><strong>Consistent User Feedback</strong>: Combine skeleton loading with other feedback mechanisms, such as progress bars or subtle loaders for better UX.</p>
</li>
</ul>
</li>
</ul>
<p>    With a bit of creativity, you can take this feature to the next level, adding your animations and personal touches. Happy no-coding!</p>
<p>    <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727022636628/6e5a9b81-fa8f-4df3-8505-c7178aea4f3c.png" alt="Help me write more for you" class="image--center mx-auto" /></p>
<h3 id="heading-help-me"><strong>Help me!</strong></h3>
<p>    If you enjoyed this post and found it helpful, Kindly consider supporting my work by buying me a coffee! Your support helps me create more valuable content and continue sharing useful resources. Thank you!</p>
]]></content:encoded></item><item><title><![CDATA[How can openAI's markdown text be converted into HTML text with CSS style without code in Bubble? (Without Paid Plugin)]]></title><description><![CDATA[As you can see in the picture above, Open AI responds in markdown text format. This format is not best for the User Experience point of view. Here is how I made it look beautiful without a paid plugin and code.
Step 1: Install this free plugin

🤌 Ma...]]></description><link>https://anishgandhi.com/markdown-to-html</link><guid isPermaLink="true">https://anishgandhi.com/markdown-to-html</guid><category><![CDATA[markdown to html]]></category><category><![CDATA[openai]]></category><category><![CDATA[bubble.io]]></category><category><![CDATA[bubble]]></category><category><![CDATA[Bubble Developers]]></category><category><![CDATA[No Code]]></category><category><![CDATA[nocode]]></category><category><![CDATA[ no-code platform]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Wed, 11 Sep 2024 07:54:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726167216136/dd87ed68-63a2-481d-883f-c7b624537634.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As you can see in the picture above, Open AI responds in markdown text format. This format is not best for the User Experience point of view. Here is how I made it look beautiful without a paid plugin and code.</p>
<h3 id="heading-step-1-install-this-free-plugin">Step 1: Install this free plugin</h3>
<ul>
<li>🤌 Markdown Pro is a free plugin created by Rico and by doing so he has done a great service to the bubble community. Here is the plugin Link: <a target="_blank" href="https://bubble.io/plugin/%F0%9F%A4%8C-markdown-pro-1664737464989x536991279033614340">https://bubble.io/plugin/%F0%9F%A4%8C-markdown-pro-1664737464989x536991279033614340</a></li>
</ul>
<h3 id="heading-step-2-setup-in-bubble-page-to-convert-from-markdown-text-format">Step 2: Setup in Bubble Page to convert from Markdown text format</h3>
<ul>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726036231623/eba43ecf-a7e7-4038-98f8-cd04065b207f.png" alt="makrdown to html element" class="image--center mx-auto" /></p>
<p>  As you can see in the above image, there is an element called ‘🤌md-to-html’ that you will find on the design tab after installation of the plugin.</p>
</li>
<li><p>Then set the element as shown below:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726036578793/df2a4c21-b57b-45dd-8e33-615d2ef06500.png" alt class="image--center mx-auto" /></p>
<p>  Make sure you provide Open AI’s response in ‘md to convert’ input of element.</p>
</li>
<li><p>Select a pre-defined style as shown in image below:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726036790938/b71aba49-7263-493f-a975-7767351bf022.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-step-3-setup-in-bubble-page-to-show-converted-content">Step 3: Setup in bubble page to show converted content</h3>
</li>
<li><p>Add an HTML Element on the page as a visual output of Open AI’s response where the User will see the output of their prompt.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726037103648/0814d0d7-6db8-41c4-bbea-d76631e49fdd.png" alt class="image--center mx-auto" /></p>
<p>  Here is how you set the output:</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726037285049/b58b429e-00bc-442f-8f35-40067d84dcb8.png" alt class="image--center mx-auto" /></p>
<p>Now run this on your Page.<br />Here are the overall steps we followed to achieve this:</p>
<ul>
<li><p>Receive Open AI output</p>
</li>
<li><p>Pass that output into markdown text to the HTML element</p>
</li>
<li><p>show that HTML converted text into HTML output.</p>
<p>  Hope this will reduce your efforts!</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727022818779/ddb48a6e-c315-482e-9a41-43b1a9930f77.png" alt="Help me write more content for you" class="image--center mx-auto" /></p>
<h3 id="heading-help-me"><strong>Help me!</strong></h3>
<p>If you enjoyed this post and found it helpful, Kindly consider supporting my work by buying me a coffee! Your support helps me create more valuable content and continue sharing useful resources. Thank you!</p>
]]></content:encoded></item><item><title><![CDATA[How to implement OAuth 2 in Bubble?]]></title><description><![CDATA[What is OAuth 2?
OAuth 2 is a way for apps to get permission to access your information without needing your password. It uses tokens, which are like temporary keys, to give specific access to your data. Imagine a valet key for your car that only all...]]></description><link>https://anishgandhi.com/how-to-implement-oauth-2-in-bubble</link><guid isPermaLink="true">https://anishgandhi.com/how-to-implement-oauth-2-in-bubble</guid><category><![CDATA[bubble.io]]></category><category><![CDATA[bubble]]></category><category><![CDATA[APIs]]></category><category><![CDATA[REST API]]></category><category><![CDATA[oauth]]></category><category><![CDATA[OAuth2]]></category><category><![CDATA[OAuth 2.0]]></category><category><![CDATA[oauth2.0]]></category><category><![CDATA[Salesforce]]></category><category><![CDATA[api integration]]></category><category><![CDATA[token]]></category><category><![CDATA[TokenManagement]]></category><category><![CDATA[API token management]]></category><dc:creator><![CDATA[Anish Gandhi]]></dc:creator><pubDate>Sat, 27 Jul 2024 14:22:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722089945879/06b63e24-f84d-4678-b091-af295f0f88a0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-what-is-oauth-2">What is OAuth 2?</h2>
<p>OAuth 2 is a way for apps to get permission to access your information without needing your password. It uses tokens, which are like temporary keys, to give specific access to your data. Imagine a valet key for your car that only allows driving, not accessing the trunk. This keeps your main password safe and only grants limited access. It's widely used by services like Google, LinkedIn, Salesforce, Zoho, and Facebook to let other apps connect securely.</p>
<h2 id="heading-how-to-start-with-basic-setup">How to start? With basic setup</h2>
<p>For this, let's take an example of Salesforce CRM. Why? because there is already too much content for Google and Facebook. lol :) and on a serious note, the Concept of OAuth 2 is the same for all.</p>
<p>Create an account on the Salesforce website, verify your email address, and fill in all basic information. Open <a target="_blank" href="https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_flows.htm&amp;type=5">Salesforce OAuth API Documentation</a> in another tab.</p>
<p>Now Go to Settings -&gt; Open Advance Setup -&gt; Home -&gt; APPs -&gt; APP Manager -&gt; New Connected APP</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722064267847/43b73d4e-6069-42b8-8165-7ae4d35b1bff.png" alt="Create new connected app in salesforce" class="image--center mx-auto" /></p>
<p>Then set up your new application like I have mentioned below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722070411983/612773c3-9044-43fc-aa75-7c73f8a9d432.png" alt="Enable OAuth and define scope in salesforce connected app" class="image--center mx-auto" /></p>
<p>Now Here is some concept clarification:</p>
<h2 id="heading-how-does-this-oauth-2-flow-work">How does this OAuth 2 flow work?</h2>
<ul>
<li><p>The bubble app will request an authorization code</p>
</li>
<li><p>Salesforce will either accept or deny the request and redirect the user to <strong>redirect URI</strong></p>
</li>
<li><p>If Accepted, the redirect URI will have an <strong>authorization code</strong> as a query parameter</p>
</li>
<li><p>Based on the authorization code, the bubble app will ask for a token exchange</p>
</li>
<li><p>In exchange for the authorization code, the bubble app will receive an <strong>access token</strong> and a <strong>refresh token</strong></p>
</li>
<li><p>Based on the access token, the bubble app will ask for details authorized in the API <strong>Scope</strong></p>
</li>
<li><p>the access token will expire</p>
</li>
<li><p>bubble app will ask for details next time and if the access token is expired, bubble app will use a refresh token to get a new access token and ask for details again via a new access token</p>
</li>
</ul>
<p>Let's make ourselves familiar with the basic definitions of keywords we are going to use.</p>
<h3 id="heading-what-is-redirect-uri">What is redirect URI?</h3>
<p>In OAuth 2, a redirect URI (Uniform Resource Identifier) is the URL to which the authorization server sends the user back after they have granted or denied permission to the application. It acts as a callback URL where the authorization code or access token is sent. This URI must be pre-registered with the authorization server to ensure the response is sent to a trusted destination, enhancing security. <strong>It's like a return address on a letter, telling the server where to send the response. This address must be registered in advance to ensure it’s safe and trusted.</strong></p>
<h3 id="heading-what-is-an-authorization-code">What is an authorization code?</h3>
<p>An authorization code in OAuth 2 is a short-lived code that an app receives after a user grants permission. The app exchanges this code for an access token, which it uses to access the user's data. This process keeps the user's credentials secure because the app never sees the user's password directly.</p>
<h3 id="heading-what-is-an-access-token">What is an access token?</h3>
<p>An access token in OAuth 2 is a security credential that allows an app to access a user's data from another service. After the user grants permission, the app receives this token and uses it to make authorized requests. It's temporary and limited to specific actions, ensuring secure and controlled access to the user's information. The app uses this key to do specific things, like read your emails or access your photos, without needing your password. It's temporary and only works for the actions you allow via scope.</p>
<h3 id="heading-what-is-a-refresh-token">What is a refresh token?</h3>
<p>A refresh token in OAuth 2 is a special token that allows an app to obtain a new access token after the current one expires. This helps the app maintain access to the user's data without needing them to log in again. It's used to ensure continuous and secure access over a longer period.</p>
<h3 id="heading-what-is-the-scope-in-oauth-2">What is the scope in OAuth 2?</h3>
<p>In OAuth 2, a scope is a parameter that specifies the level of access the app is requesting from the user. It defines what actions the app can perform and what data it can access. For example, a scope might allow an app to read your emails but not send them. This helps users understand and control what permissions they are granting to the app.</p>
<h3 id="heading-what-is-client-id-in-oauth-2">What is client ID in OAuth 2?</h3>
<p>In OAuth 2, a client ID is a unique identifier assigned to an app when it registers with an authorization server. It's used to identify the app requesting the OAuth flow. Think of it like a username for the app, which helps the authorization server know which app is requesting access to user data.</p>
<h3 id="heading-what-is-client-secret-in-oauth-2">What is client secret in OAuth 2?</h3>
<p>In OAuth 2, a client secret is a confidential key assigned to an app when it registers with an authorization server. It's used along with the client ID to authenticate the app and ensure that the request is coming from a trusted source. Think of it like a password for the app, helping to keep the communication secure.</p>
<p>You can find the client ID and secret in the manage connected app section of settings shown below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722077181011/962249c9-5c15-4c26-8090-7383d1564c54.png" alt="Find client id and client secret in salesforce" class="image--center mx-auto" /></p>
<h3 id="heading-what-does-content-type-as-applicationx-www-form-urlencoded-mean">What does content type as application/x-www-form-urlencoded mean?</h3>
<p>The content type <code>application/x-www-form-urlencoded</code> indicates that data sent in an HTTP request is encoded as key-value pairs, separated by <code>&amp;</code> and with each key and value separated by <code>=</code>. This format is commonly used when submitting form data via HTTP POST requests. For example, <code>name=John&amp;age=30</code> is a typical representation of this content type.</p>
<h2 id="heading-how-to-setup-up-oauth-api-calls-in-bubble-app">How to Setup up OAuth API calls in Bubble APP?</h2>
<h3 id="heading-step-1-request-authorization-code">Step 1: Request authorization code</h3>
<p>Here is the API doc link for this.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722077694815/584376cb-d416-4ce5-a50c-a4c296c80256.png" alt="Reuqest authorization code from bubble app" class="image--center mx-auto" /></p>
<p>In the bubble app, Create a button from which you need to initiate authorization and create the action 'Open an external website'. The destination will be similar to that shown in the image below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722076240455/7dfc5106-1973-4854-a81e-4b661e4bf1ff.png" alt="Requesting authorization code via opening external site" class="image--center mx-auto" /></p>
<p>Now 'MyDomainName' is the same as your salesforce account subdomain which you can see in the URL when you open your your salesforce account.</p>
<p>When the user clicks on this button, the bubble app will redirect you to this URL and it will ask you to log into a Salesforce account then it will redirect to redirect URI you had set up and added in the button link.</p>
<p>This URL will have a query parameter named 'code'. For me, it was like this: <a target="_blank" href="https://unicotasky.bubbleapps.io/version-test/salesforce?code=aPrxI5eherH6PXZaIC0jwHd4ehTG6AIWy5hT6F7Wa10T5A.btFkMQdrYdJ2j.pA714_oiXiw6g%3D%3D">https://unicotasky.bubbleapps.io/version-test/salesforce?code=aPrxI5eherH6PXZaIC0jwHd4esNOZqUHE_ub76CHD.JJj5W5iU54fhOCG87qYsSA3d8e65UtOA%3D%3D</a></p>
<h3 id="heading-step-2-request-access-token">Step 2: Request access token</h3>
<p>Now we have to set up the request access token. Here is the <a target="_blank" href="https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_web_server_flow.htm&amp;language=en_US&amp;type=5">Salesforce API Doc</a></p>
<p>The header will have the content type as application/x-www-form-urlencoded</p>
<p>Body parameters will have grant_type(static), code(dynamic), client_id (static), client_secret(static) and redirect_url(static).</p>
<p>In the Bubble API connector, it will look like this</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722080037265/9e247073-73b2-45d4-b30e-507a010280ac.png" alt="Request access token bubble app OAuth 2" class="image--center mx-auto" /></p>
<p>Now put the code here and initialize the call, then save the response.</p>
<p>Make sure to convert the code as URL Parameter to text.</p>
<p>This means code=aPrxI5eherH6PXZaIC0jwHd4ehTG6AIWy5hT6F7Wa10T5A.btFkMQdrYdJ2j.pA714_oiXiw6g%3D%3D</p>
<p>is code=aPrxI5eherH6PXZaIC0jwHd4ehTG6AIWy5hT6F7Wa10T5A.btFkMQdrYdJ2j.pA714_oiXiw6g==</p>
<p>when you are using this in the body of an API call.</p>
<p>Successfully initialized call will look like this</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722080604671/1d123f16-6b7b-4159-a287-7c2678aa4d47.png" alt="Successfully initialised request access call" class="image--center mx-auto" /></p>
<p>Once initialized, Create a workflow on your redirect URL Page which you can execute either on 'Page load' or 'Do when condition is true'</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722081778142/09c4870e-aaf3-49e6-ab72-f7f98c9312c9.png" alt="Workflow Request access token" class="image--center mx-auto" /></p>
<p>Then I save the response to this action into the database in which I need access, token, refresh token, and expired by. <strong>Make sure to protect this data with privacy rules!</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722081992072/9c2b16d8-4cdc-4f4d-a38b-6302c9b09f4e.png" alt="Workflow store token" class="image--center mx-auto" /></p>
<h3 id="heading-step-3-get-data-before-the-access-token-expires">Step 3: Get Data before the access token expires</h3>
<p>Here is the <a target="_blank" href="https://resources.docs.salesforce.com/latest/latest/en-us/sfdc/pdf/api_rest.pdf">Salesforce API Doc</a> reference.</p>
<p>Now Suppose you want to get data on accounts from the salesforce account of your user. You have an access token in DB that is not expired.</p>
<p>Here is the endpoint you need to use for this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722087610457/56b373eb-6673-4e6b-9594-45ecc4581508.png" alt="API Call Doc for Get data from Salesforce" class="image--center mx-auto" /></p>
<p>In Bubble, the API Setup will be like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722087887100/e260cf06-cc33-4b07-9515-c6697537ec8b.png" alt class="image--center mx-auto" /></p>
<p>and When successfully initialised call will look like this</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722087971931/a56bae40-1b55-4f42-8e33-fd239673a675.png" alt="Successfull Get Data API Call" class="image--center mx-auto" /></p>
<p>Now I am creating a button which will only show If I have completed Step 2 successfully. As you can see, the data is empty, I haven't fetched it yet.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722088115469/12f8d7fd-e4e3-46da-9b7d-6397ae7aeff8.png" alt="Show Data from sales force button" class="image--center mx-auto" /></p>
<p>Now I am checking whether the token is expired or not.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722088757778/8407d6c7-3dda-4395-af20-fd63c91f8d76.png" alt="Access token not expired" class="image--center mx-auto" /></p>
<p>When it is not expired, I will fetch the data from salesforce and show that in Repeating group.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722088881733/e09c0b70-aa1e-4eef-bbea-14184c42372c.png" alt="Get data using access token" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722088951725/69ec4f54-c99a-4676-a8e4-d748cc3bf2b4.png" alt="display sata from salesforce accounts" class="image--center mx-auto" /></p>
<p>Vola! Data from salesforce is visible now:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722089022733/32636403-450e-49d1-bcb9-f76684186fc0.png" alt="Successfull data fetch from salesforce" class="image--center mx-auto" /></p>
<h3 id="heading-step-4-get-data-after-the-access-token-expires">Step 4: Get Data after the access token expires</h3>
<p>When you click the button and that time is greater than expire time, it means that access token is expired and we need new access token. So we will use refresh token to get it. Here is what the workflow will look like</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722089426496/a8569a93-4b06-4dc4-bf31-efa2e1216921.png" alt="Refresh token to get new access token" class="image--center mx-auto" /></p>
<p>This is how OAuth 2 works for all the platforms. There might be minor changes so always refer to API documentation to understand the nitty-gritty of their APIs.</p>
<p>Hope this will help you learn more about OAuth 2 and APIs in Bubble.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727023139694/c7eb8299-ddcb-4581-84d7-75ac167337b5.png" alt="Help me write more content for you" class="image--center mx-auto" /></p>
<h3 id="heading-help-me"><strong>Help me!</strong></h3>
<p>If you enjoyed this post and found it helpful, Kindly consider supporting my work by buying me a coffee! Your support helps me create more valuable content and continue sharing useful resources. Thank you!</p>
<p>These API calls are not secure. <a target="_blank" href="https://anishgandhi.com/bubble-security-api-best-practices">Click here</a> to find out how to secure them.</p>
]]></content:encoded></item></channel></rss>