supaguardsupaguardDocs
Concepts

Shared Modules: Reuse Code Across Monitoring Checks

Write reusable TypeScript modules and share them across all your monitoring checks. Build page objects, auth helpers, and utility functions once — use them everywhere.

Shared modules let you write reusable TypeScript code — page objects, auth helpers, utility functions — and import them into any monitoring check in your organization. Write once, use everywhere.

The Problem: Duplicate Code Across Checks

As your monitoring coverage grows, you'll notice patterns repeating across checks:

  • Login flows shared by 10+ checks
  • Common selectors for navigation, modals, or form components
  • Helper functions for date formatting, data generation, or assertions
  • API authentication logic used in every API check

Without shared modules, you end up copy-pasting the same code into every check script. When your UI changes, you update it in one check and forget the other nine.

How Shared Modules Work

Shared modules are TypeScript files stored per-organization in supaguard. When a check runs, all your organization's modules are bundled alongside the check script and made available via the @org/* import alias.

your-check.spec.ts          →  imports from @org/*
@org/pages/LoginPage.ts      →  shared login page object
@org/helpers/auth.ts         →  shared auth helpers
@org/helpers/dates.ts        →  shared date utilities

The flow:

  1. Write a module locally (e.g., LoginPage.ts)
  2. Push it to your organization with the CLI
  3. Import it in any check using @org/path/to/module
  4. Run — supaguard bundles the module with your check automatically

Pushing Modules

Use the supaguard CLI to push a module to your organization:

supaguard modules push ./src/pages/LoginPage.ts --path "pages/LoginPage.ts"

The --path flag sets the import path. If omitted, it defaults to the filename. After pushing, any check in your organization can import from @org/pages/LoginPage.

Upsert behavior: If a module with the same path already exists, it's updated automatically. No need to delete and re-create.

Managing modules

# List all modules in your organization
supaguard modules list

# Delete a module
supaguard modules delete <module-id>

Importing Modules in Checks

Use the @org/* prefix to import shared modules in any check script:

import { test, expect } from "@playwright/test";
import { LoginPage } from "@org/pages/LoginPage";
import { generateTestEmail } from "@org/helpers/utils";

test("dashboard loads after login", async ({ page }) => {
  const loginPage = new LoginPage(page);
  const email = generateTestEmail();

  await loginPage.goto();
  await loginPage.loginWithEmail(email, process.env.TEST_PASSWORD);

  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
});

Modules can also import from other modules:

// @org/pages/LoginPage.ts
import { fillAndSubmitForm } from "@org/helpers/forms";

export class LoginPage {
  constructor(private page: any) {}

  async goto() {
    await this.page.goto("https://app.example.com/login");
  }

  async loginWithEmail(email: string, password: string) {
    await fillAndSubmitForm(this.page, {
      "input[name='email']": email,
      "input[name='password']": password,
    });
  }
}

Examples

Page object pattern

The most common use case — wrap page interactions in a reusable class:

// Push as: supaguard modules push LoginPage.ts --path "pages/LoginPage.ts"

import { expect, type Page } from "@playwright/test";

export class LoginPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto("https://app.example.com/login");
  }

  async loginWithEmail(email: string, password: string) {
    await this.page.fill("[data-testid='email']", email);
    await this.page.fill("[data-testid='password']", password);
    await this.page.click("button[type='submit']");
    await expect(this.page.getByText("Welcome")).toBeVisible();
  }

  async loginWithGoogle() {
    await this.page.click("a:has-text('Sign in with Google')");
  }
}

Auth helper

Centralize authentication logic so every check starts from an authenticated state:

// Push as: supaguard modules push auth.ts --path "helpers/auth.ts"

import type { Page } from "@playwright/test";

export async function authenticateUser(
  page: Page,
  email: string,
  password: string,
) {
  await page.goto("https://app.example.com/login");
  await page.fill("[data-testid='email']", email);
  await page.fill("[data-testid='password']", password);
  await page.click("button[type='submit']");
  await page.waitForURL("**/dashboard");
}

Test data generator

Generate unique data for each check run to avoid test pollution:

// Push as: supaguard modules push data.ts --path "helpers/data.ts"

import { faker } from "@faker-js/faker";

export function generateUser() {
  return {
    email: `monitor+${Date.now()}@example.com`,
    name: faker.person.fullName(),
    company: faker.company.name(),
  };
}

export function generateOrderId() {
  return `ORD-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
}

Reuse common navigation patterns across checks:

// Push as: supaguard modules push nav.ts --path "helpers/nav.ts"

import { expect, type Page } from "@playwright/test";

export async function navigateTo(page: Page, section: string) {
  await page.click(`nav a:has-text("${section}")`);
  await expect(page.getByRole("heading", { level: 1 })).toContainText(section);
}

export async function openSettingsTab(page: Page, tab: string) {
  await page.goto("https://app.example.com/settings");
  await page.click(`[role="tab"]:has-text("${tab}")`);
}

Path Rules

Module paths must follow these rules:

  • Must end in .ts or .js
  • Can only contain alphanumeric characters, underscores, hyphens, and forward slashes
  • Cannot contain .. (no path traversal)
  • Cannot start with / (must be relative)

Valid paths:

  • LoginPage.ts
  • pages/LoginPage.ts
  • helpers/auth/login.ts
  • page-objects/HomePage.ts

Allowed Dependencies

Modules run in a sandboxed environment. You can import from:

PackageDescription
@playwright/testPlaywright test utilities
@org/*Other shared modules in your organization
@faker-js/fakerTest data generation
dayjs, date-fnsDate manipulation
lodash, lodash-esUtility functions
uuidUUID generation
zodSchema validation

Blocked: Node.js built-ins (fs, net, http, child_process, etc.), eval(), Function(), and dynamic import() statements are not allowed for security.

Best Practices

Organize by concern

@org/
  pages/           # Page object models
    LoginPage.ts
    DashboardPage.ts
    CheckoutPage.ts
  helpers/         # Utility functions
    auth.ts
    data.ts
    nav.ts
    assertions.ts

Keep modules focused

Each module should have a single responsibility. A LoginPage module handles login — it doesn't also manage the dashboard.

Use environment variables for secrets

Never hardcode credentials in modules. Use process.env and configure values in your organization's environment variables:

// Good
await loginPage.loginWithEmail(
  process.env.TEST_EMAIL,
  process.env.TEST_PASSWORD,
);

// Bad — credentials in source code
await loginPage.loginWithEmail("admin@example.com", "secret123");

Version through path conventions

When making breaking changes to a module, consider a versioned path:

supaguard modules push LoginPage-v2.ts --path "pages/LoginPage-v2.ts"

This lets you migrate checks one at a time instead of breaking all of them at once.

On this page