DEV Community

Cover image for Building a Full-Stack Micro-SaaS Inside n8n: Serverless Web Apps, DataTables & AI Image Pipelines
Ruesch Manny
Ruesch Manny

Posted on

Building a Full-Stack Micro-SaaS Inside n8n: Serverless Web Apps, DataTables & AI Image Pipelines

Most developers treat n8n strictly as an orchestrator for backend APIsβ€”triggering on webhooks, transforming JSON, and sending Slack alerts.

However, by pairing n8n's Webhook node with the Respond to Webhook node, you can turn n8n into a complete serverless full-stack web application host. No Vercel, no Next.js deployment, no Docker frontend containers required.

In this deep dive, we'll examine how we built an entire AI Virtual Staging Micro-SaaS that lives 100% inside an n8n workflowβ€”including:

  1. Serving an interactive, responsive SPA with embedded Before/After comparison sliders.
  2. Performing client-side HTML5 Canvas compression to prevent payload timeouts.
  3. Implementing non-blocking asynchronous job polling using native n8n DataTables.
  4. Enforcing architectural prompt guards to freeze room geometry in image-to-image AI pipelines.

AI Virtual Home Staging Studio β€” Before & After Showcase
Figure 1: Production pipeline output β€” empty Berlin Altbau listing photo (left) transformed into curated luxury staging (right) in ~33 seconds on Google Gemini compute.


πŸ—οΈ The Serverless n8n Web App Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   CLIENT BROWSER                       β”‚
β”‚  - Vanilla JS SPA (Served directly by n8n)            β”‚
β”‚  - HTML5 Canvas client-side compression (<3MB)         β”‚
β”‚  - Interactive Before/After slider                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚ (1) GET /webhook/home-staging-form
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            n8n Webhook: "Page HTML"                    │───► Sends raw text/html; charset=utf-8
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚ (2) POST /webhook/home-staging-submit
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     Access Gate & Honeypot Validation                  │───► Validates PIN & daily quota
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚ (3) Returns { "jobId": "..." } immediately (<200ms)
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Async Job Pipeline (Brain -> Gemini -> Drive)        │───► Asynchronous execution
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚ (4) Client polls /home-staging-status?jobId=...
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Status Node: Reads from n8n DataTable             │───► Status: 'pending' -> 'done'
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Pattern 1: Serving Full-Stack HTML Directly via n8n

To serve an entire web app from n8n without external static hosting:

  1. Create a Webhook node set to GET on /webhook/home-staging-form with Response Mode: When Last Node Finishes.
  2. Connect a Code node (Page HTML) that returns your HTML, embedded CSS, and vanilla JS application as a template literal string:
// Inside 'Page HTML' Code Node
const BRAND = {
  name: "Studio AI",
  colorPrimary: "#133b5c",
  colorAccent: "#2a6f97"
};

const html = `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>${BRAND.name}</title>
  <style>
    :root { --primary: ${BRAND.colorPrimary}; --accent: ${BRAND.colorAccent}; }
    body { font-family: system-ui, sans-serif; display: flex; justify-content: center; }
    /* Interactive Before/After Slider CSS */
    .cmp { position: relative; overflow: hidden; cursor: ew-resize; }
    .cmp .before { position: absolute; inset: 0; clip-path: inset(0 calc(100% - var(--pos, 50%)) 0 0); }
  </style>
</head>
<body>
  <div class="card">
    <h1>${BRAND.name}</h1>
    <form id="f">
      <input type="file" id="photo" accept="image/*">
      <button type="submit">Transform Room</button>
    </form>
  </div>
  <script>
    // Client-side execution logic...
  </script>
</body>
</html>`;

return [{ json: { html } }];
Enter fullscreen mode Exit fullscreen mode
  1. Connect a Respond to Webhook node with:
    • Respond With: text
    • Response Body: ={{ $json.html }}
    • Header: Content-Type: text/html; charset=utf-8

When visitors navigate to your n8n webhook URL, n8n responds instantly with a full graphical interface.


Pattern 2: Client-Side Compression Before Webhook Submission

Raw smartphone photos from real estate agents frequently exceed 12 MB to 20 MB. Uploading raw payloads directly to an automation webhook risks gateway timeouts (504) or memory spikes in the n8n container.

We solve this entirely in the browser using HTML5 Canvas before FormData is dispatched:

async function compressImage(file) {
  const bmp = await createImageBitmap(file, { imageOrientation: 'from-image' });
  const MAX = 2048;
  const scale = Math.min(1, MAX / Math.max(bmp.width, bmp.height));

  if (scale >= 1 && file.size < 3 * 1024 * 1024) return file;

  const canvas = document.createElement('canvas');
  canvas.width = Math.round(bmp.width * scale);
  canvas.height = Math.round(bmp.height * scale);

  const ctx = canvas.getContext('2d');
  ctx.drawImage(bmp, 0, 0, canvas.width, canvas.height);

  const blob = await new Promise(res => canvas.toBlob(res, 'image/jpeg', 0.88));
  return new File([blob], 'photo.jpg', { type: 'image/jpeg' });
}
Enter fullscreen mode Exit fullscreen mode

This reduces 15 MB iPhone photos to under 1.8 MB in under 200 milliseconds, eliminating upload failures.


Pattern 3: Asynchronous Job Polling via Native n8n DataTables

Synchronous image generation calls take 15 to 30 seconds. Keeping an HTTP connection open between the browser and n8n for 30 seconds is fragile (cellular disconnects, proxy timeouts).

Instead, we implement a two-phase asynchronous job queue using n8n's native DataTables:

  1. Submission (POST /home-staging-submit):
    • The webhook receives the image and parameters.
    • Generates an unguessable jobId (crypto.randomUUID()).
    • Inserts a row into the staging_jobs DataTable: status = "pending".
    • Immediately returns { "jobId": "..." } to the client in <200ms.
  2. Background Processing:
    • The workflow continues downstream without blocking the client.
    • Brain node builds the AI prompt.
    • Google Gemini edits the photo.
    • Result is saved to Google Drive.
    • Updates the DataTable row: status = "done", driveFileId = "...".
  3. Client Polling (GET /home-staging-status?jobId=...):
    • The browser polls every 4 seconds.
    • A tiny webhook reads the row from DataTable and returns { "status": "done" }.
    • Once "done", the browser fetches the image from /home-staging-image?jobId=... and activates the interactive slider.

Pattern 4: Strict Architectural Prompt Engineering (The Camera Lock)

When working with image-to-image models (like Google Gemini 3 Pro Image / Nano Banana Pro), models often hallucinate extra windows, alter wall geometry, or shift the camera angle. In real estate, an edited room that changes the physical architecture is legally unusable.

To prevent this, every dynamic prompt ends with an immutable Camera Lock Rule:

const cameraLock = " [FINAL HARD CONSTRAINTS - VERIFY BEFORE OUTPUT]: " +
  "This is an in-place edit of the input photograph. " +
  "1) Camera position, angle, framing, perspective, and focal length must be PIXEL-IDENTICAL to the input photo. " +
  "2) Room geometry is FROZEN: same walls, same ceiling, same windows, same doors, same floor area - never extend or reproportion the room. " +
  "3) Do NOT add ceiling-mounted or architectural elements: no extra windows, no fireplaces, no moldings. " +
  "4) Added furniture and decor must fit entirely WITHIN the space visible in the original photo.";
Enter fullscreen mode Exit fullscreen mode

Because image generation models place heavier attention weights on final tokens, this prompt structure guarantees architectural fidelity.


Conclusion & Production Workflow

By combining webhook-served HTML, client-side Canvas optimization, and native n8n DataTables for polling, you can build production-grade, full-stack Micro-SaaS tools inside a single n8n workspace.

Have you used n8n to serve client-facing frontend apps? Share your architecture in the comments below!

Top comments (0)