IsUp.dev - Project Overview & Technical PRD

Executive Summary

IsUp (https://isup.dev) is a modern uptime monitoring service built on the do.dev platform. It integrates with the existing do-dev authentication system and billing-dev billing infrastructure, providing a seamless experience for users across the do.dev ecosystem.

Project ID: isup-dev App ID: isup (for multi-tenant Convex tables) Domain: isup.dev Target Launch: Q2 2026


Platform Integration

do.dev Ecosystem

IsUp is part of the do.dev family of products:

┌─────────────────────────────────────────────────────────────────────┐
│                        do.dev Ecosystem                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   ┌─────────────┐  ┌─────────────┐  ┌─────────────┐                │
│   │   do-dev    │  │ billing-dev │  │  isup-dev   │  ... more      │
│   │   (Core)    │  │  (Billing)  │  │ (Monitoring)│                │
│   └──────┬──────┘  └──────┬──────┘  └──────┬──────┘                │
│          │                │                │                        │
│          └────────────────┼────────────────┘                        │
│                           │                                         │
│                           ▼                                         │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │                    Shared Infrastructure                     │  │
│   │                                                              │  │
│   │  • WorkOS Auth (SSO, OAuth)                                 │  │
│   │  • Convex (Real-time Database)                              │  │
│   │  • Stripe (Payments via billing-dev)                        │  │
│   │  • @workspace/ui (Shared Components)                        │  │
│   │  • @do/catalyst (Tailwind Components)                       │  │
│   │                                                              │  │
│   └─────────────────────────────────────────────────────────────┘  │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Shared Dependencies

DependencyPurposeSource
WorkOS AuthKitAuthentication, SSOdo-dev
ConvexReal-time database, functions@workspace/convex
billing-dev APISubscriptions, entitlements, usagebilling-dev
@workspace/uiRadix UI componentspackages/ui
@do/catalystTailwind componentspackages/catalyst
ResendEmail notificationsShared
SentryError trackingShared
PostHogAnalyticsShared

Authentication Flow

WorkOS Integration (Same as do-dev)

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   isup.dev   │────▶│    WorkOS    │────▶│   Convex     │
│   Frontend   │     │   AuthKit    │     │   Users      │
└──────────────┘     └──────────────┘     └──────────────┘


                     ┌──────────────┐
                     │  OAuth:      │
                     │  • Google    │
                     │  • GitHub    │
                     │  • Email/PW  │
                     └──────────────┘

User Record Structure (extends do-dev pattern):

// Convex users table (shared schema)
{
  _id: Id<"users">,
  name: string,
  email: string,
  image?: string,

  // Multi-app support
  appId: "isup",  // App identifier

  // External IDs
  userId: "usr_xxx",      // External user ID
  custId: "cus_xxx",      // Billing customer ID
  workosUserId: string,   // WorkOS user ID
  organizationId: "org_xxx",

  // Auth metadata
  authProvider: "workos" | "google" | "github",
  verified: boolean,
  roles: string[],        // ["user", "admin", etc.]

  // Timestamps
  createdAt: number,
  lastLoginAt: number,
}

WorkOS Metadata Sync:

// Metadata stored in WorkOS (same pattern as do-dev)
{
  roles: string,           // JSON stringified
  userId: string,
  convexUserId: string,
  billingOrgId: string,
  plan: string,
  appId: "isup",
}

Billing Integration

billing-dev API Usage

IsUp uses billing-dev for all subscription management:

┌──────────────┐                      ┌──────────────┐
│   isup-dev   │──── HTTP API ───────▶│  billing-dev │
│              │                      │              │
│  • Checkout  │                      │  • Stripe    │
│  • Portal    │                      │  • Plans     │
│  • Usage     │                      │  • Credits   │
└──────────────┘                      └──────────────┘

API Endpoints Used

EndpointPurpose
POST /checkoutCreate Stripe checkout session
POST /portalOpen Stripe customer portal
GET /entitlement?userId=Get plan limits & features
GET /subscription?userId=Get subscription status
POST /usage/recordRecord API/monitor usage
POST /cancelCancel subscription

IsUp Plans (billing-dev products)

Add to billing-dev products table:

SlugTypePriceIncluded Limits
isup-hobbybase_plan$010 monitors, 5min interval, 3mo retention
isup-probase_plan$9/mo50 monitors, 1min interval, 12mo retention
isup-teambase_plan$29/mo200 monitors, 30sec interval, 24mo retention
isup-enterprisebase_planCustomUnlimited, 15sec interval, unlimited retention

Entitlement Features

// Features to add to billing-dev products
features: [
  "isup.access",           // Base access
  "isup.api",              // REST API access
  "isup.status_pages",     // Status page feature
  "isup.white_label",      // White-label status pages
  "isup.ssl_monitoring",   // SSL cert monitoring
  "isup.dns_monitoring",   // DNS monitoring
  "isup.sms_alerts",       // SMS notifications
  "isup.voice_alerts",     // Voice call notifications
  "isup.team_members",     // Multiple team members
  "isup.advanced_analytics", // Detailed analytics
]

Usage Metering

Track in billing-dev's usage system:

// Record monitor check usage
POST /usage/record
{
  orgId: "org_xxx",
  domain: "isup",
  endpoint: "monitor.check",
  creditsCost: 1,  // 1 credit per check (or 0 for included)
  metadata: {
    monitorId: "mon_xxx",
    region: "us-east",
    responseTime: 234,
  }
}

Database Schema (Convex)

IsUp-Specific Tables

Add to @workspace/convex schema:

// tools/convex/convex/schema.ts additions

// ============================================
// IsUp Tables (appId: "isup")
// ============================================

monitors: defineTable({
  // Ownership
  appId: v.literal("isup"),
  userId: v.id("users"),
  organizationId: v.string(),  // org_xxx

  // Monitor config
  name: v.string(),
  type: v.union(
    v.literal("http"),
    v.literal("ping"),
    v.literal("port"),
    v.literal("keyword"),
    v.literal("ssl"),
    v.literal("dns"),
    v.literal("heartbeat")
  ),
  url: v.string(),

  // Check settings
  intervalSeconds: v.number(),  // 60, 180, 300, etc.
  timeoutSeconds: v.number(),
  regions: v.array(v.string()), // ["us-east", "eu-west", "asia"]

  // HTTP-specific
  httpMethod: v.optional(v.string()),
  httpHeaders: v.optional(v.any()),  // JSON object
  httpBody: v.optional(v.string()),
  expectedStatusCodes: v.optional(v.array(v.number())),

  // Keyword-specific
  keyword: v.optional(v.string()),
  keywordType: v.optional(v.union(v.literal("present"), v.literal("absent"))),

  // Port-specific
  port: v.optional(v.number()),

  // SSL-specific
  sslExpiryAlertDays: v.optional(v.array(v.number())),

  // Thresholds
  responseTimeThreshold: v.optional(v.number()),

  // State
  status: v.union(v.literal("active"), v.literal("paused"), v.literal("deleted")),
  currentState: v.union(v.literal("up"), v.literal("down"), v.literal("degraded"), v.literal("unknown")),
  lastCheckedAt: v.optional(v.number()),
  lastStateChangeAt: v.optional(v.number()),

  // Stats (denormalized for fast reads)
  uptimePercent24h: v.optional(v.number()),
  uptimePercent7d: v.optional(v.number()),
  uptimePercent30d: v.optional(v.number()),
  avgResponseTime24h: v.optional(v.number()),

  // Timestamps
  createdAt: v.number(),
  updatedAt: v.number(),
})
  .index("by_user", ["userId"])
  .index("by_org", ["organizationId"])
  .index("by_status", ["status"])
  .index("by_state", ["currentState"])
  .index("by_type", ["type"])
  .index("by_next_check", ["status", "lastCheckedAt"]),

alertContacts: defineTable({
  appId: v.literal("isup"),
  userId: v.id("users"),
  organizationId: v.string(),

  name: v.string(),
  type: v.union(
    v.literal("email"),
    v.literal("slack"),
    v.literal("discord"),
    v.literal("telegram"),
    v.literal("webhook"),
    v.literal("sms"),
    v.literal("voice")
  ),

  // Type-specific config
  config: v.any(),  // { email: "...", webhookUrl: "...", etc. }

  isVerified: v.boolean(),
  isDefault: v.boolean(),

  createdAt: v.number(),
})
  .index("by_user", ["userId"])
  .index("by_org", ["organizationId"])
  .index("by_type", ["type"]),

monitorAlertContacts: defineTable({
  monitorId: v.id("monitors"),
  alertContactId: v.id("alertContacts"),
})
  .index("by_monitor", ["monitorId"])
  .index("by_contact", ["alertContactId"]),

incidents: defineTable({
  appId: v.literal("isup"),
  monitorId: v.id("monitors"),
  organizationId: v.string(),

  // Timing
  startedAt: v.number(),
  endedAt: v.optional(v.number()),
  durationSeconds: v.optional(v.number()),

  // Details
  cause: v.string(),  // "timeout", "status_code", "ssl_expired", etc.
  errorMessage: v.optional(v.string()),
  affectedRegions: v.array(v.string()),

  // Resolution
  resolvedBy: v.optional(v.union(v.literal("auto"), v.literal("manual"))),
  notes: v.optional(v.string()),

  // Notifications sent
  notificationsSent: v.array(v.object({
    contactId: v.id("alertContacts"),
    sentAt: v.number(),
    type: v.string(),
    success: v.boolean(),
  })),

  createdAt: v.number(),
})
  .index("by_monitor", ["monitorId"])
  .index("by_org", ["organizationId"])
  .index("by_time", ["startedAt"])
  .index("by_active", ["monitorId", "endedAt"]),

statusPages: defineTable({
  appId: v.literal("isup"),
  userId: v.id("users"),
  organizationId: v.string(),

  // Page config
  slug: v.string(),
  name: v.string(),
  description: v.optional(v.string()),

  // Custom domain
  customDomain: v.optional(v.string()),
  customDomainVerified: v.optional(v.boolean()),

  // Appearance
  theme: v.optional(v.any()),  // { primaryColor, logo, favicon, etc. }

  // Access
  isPublic: v.boolean(),
  passwordHash: v.optional(v.string()),

  // SEO
  allowIndexing: v.boolean(),

  createdAt: v.number(),
  updatedAt: v.number(),
})
  .index("by_user", ["userId"])
  .index("by_org", ["organizationId"])
  .index("by_slug", ["slug"])
  .index("by_domain", ["customDomain"]),

statusPageMonitors: defineTable({
  statusPageId: v.id("statusPages"),
  monitorId: v.id("monitors"),
  displayName: v.optional(v.string()),
  sortOrder: v.number(),
})
  .index("by_page", ["statusPageId"])
  .index("by_monitor", ["monitorId"]),

statusPageAnnouncements: defineTable({
  statusPageId: v.id("statusPages"),

  title: v.string(),
  body: v.string(),
  type: v.union(
    v.literal("incident"),
    v.literal("maintenance"),
    v.literal("info")
  ),
  status: v.union(
    v.literal("investigating"),
    v.literal("identified"),
    v.literal("monitoring"),
    v.literal("resolved"),
    v.literal("scheduled"),
    v.literal("in_progress"),
    v.literal("completed")
  ),

  // For scheduled maintenance
  scheduledFor: v.optional(v.number()),
  scheduledUntil: v.optional(v.number()),

  // Updates
  updates: v.array(v.object({
    message: v.string(),
    status: v.string(),
    createdAt: v.number(),
  })),

  createdAt: v.number(),
  updatedAt: v.number(),
})
  .index("by_page", ["statusPageId"])
  .index("by_type", ["type"])
  .index("by_status", ["status"]),

Check Results (Time-Series - External)

For high-volume check results, use ClickHouse/Tinybird (not Convex):

-- Tinybird/ClickHouse schema
CREATE TABLE check_results (
  monitor_id String,
  organization_id String,
  timestamp DateTime64(3),
  region String,
  status Enum8('up' = 1, 'down' = 2, 'degraded' = 3),
  response_time_ms UInt32,
  status_code Nullable(UInt16),
  error_message Nullable(String),

  -- Detailed timing
  dns_time_ms Nullable(UInt32),
  connect_time_ms Nullable(UInt32),
  tls_time_ms Nullable(UInt32),
  ttfb_ms Nullable(UInt32),

  -- SSL info
  ssl_expiry_date Nullable(DateTime),
  ssl_issuer Nullable(String),

  -- DNS info
  dns_records Nullable(String)  -- JSON
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (organization_id, monitor_id, timestamp)
TTL timestamp + INTERVAL 90 DAY;

Monitoring Workers Architecture

Cloudflare Workers (Primary)

┌─────────────────────────────────────────────────────────────────────┐
│                    Cloudflare Workers (Edge)                         │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                   isup-checker Worker                        │   │
│   │                                                              │   │
│   │   Regions: us-east, us-west, eu-west, eu-central,           │   │
│   │            asia-east, asia-south, oceania                    │   │
│   │                                                              │   │
│   │   Triggers:                                                  │   │
│   │   • Cron: */1 * * * * (every minute)                        │   │
│   │   • Queue: check-queue (for immediate checks)               │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                      Cloudflare KV                           │   │
│   │                                                              │   │
│   │   • Monitor configs (cached)                                 │   │
│   │   • Last check times                                        │   │
│   │   • Rate limit state                                        │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                   Cloudflare Queues                          │   │
│   │                                                              │   │
│   │   • results-queue → Tinybird ingestion                      │   │
│   │   • incidents-queue → Convex mutations                      │   │
│   │   • notifications-queue → Notification workers              │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Worker Flow

// Pseudo-code for checker worker
export default {
  async scheduled(event: ScheduledEvent, env: Env) {
    const region = env.REGION;  // e.g., "us-east"

    // 1. Get monitors due for check in this region
    const monitors = await getMonitorsDue(env, region);

    // 2. Execute checks in parallel (batched)
    const results = await Promise.all(
      monitors.map(m => executeCheck(m, region))
    );

    // 3. Queue results for processing
    for (const result of results) {
      // Write to Tinybird
      await env.RESULTS_QUEUE.send(result);

      // If state changed, trigger incident flow
      if (result.stateChanged) {
        await env.INCIDENTS_QUEUE.send({
          monitorId: result.monitorId,
          newState: result.status,
          previousState: result.previousState,
          result,
        });
      }
    }
  },
};

API Routes

Next.js App Router Structure

apps/isup-dev/app/
├── (marketing)/              # Public pages
│   ├── page.tsx             # Landing page
│   ├── pricing/
│   └── docs/
├── (dashboard)/             # Protected app
│   ├── layout.tsx
│   ├── page.tsx             # Dashboard home
│   ├── monitors/
│   │   ├── page.tsx         # Monitor list
│   │   ├── [id]/
│   │   │   ├── page.tsx     # Monitor detail
│   │   │   └── settings/
│   │   └── new/
│   ├── incidents/
│   ├── status-pages/
│   ├── alerts/
│   └── settings/
├── (status)/                # Public status pages
│   └── status/[slug]/
├── api/
│   ├── auth/
│   │   └── user/route.ts    # Same pattern as do-dev
│   ├── monitors/
│   │   ├── route.ts         # CRUD
│   │   └── [id]/
│   ├── incidents/
│   ├── status-pages/
│   ├── alerts/
│   ├── billing/             # Proxy to billing-dev
│   └── webhooks/
│       ├── cloudflare/      # Worker results
│       └── stripe/          # Payment events
└── middleware.ts            # WorkOS auth (same as do-dev)

API Patterns (Same as do-dev)

// Example: GET /api/monitors
import { withAuth } from "@workos-inc/authkit-nextjs";
import { hasPermission } from "@/lib/workos";
import { api } from "@workspace/convex";
import { fetchQuery } from "convex/nextjs";

export async function GET(request: Request) {
  const { user } = await withAuth();
  if (!user) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  // Check entitlements from billing-dev
  const entitlements = await getEntitlements(user.id);
  if (!entitlements.features.includes("isup.access")) {
    return Response.json({ error: "No access" }, { status: 403 });
  }

  // Fetch from Convex
  const monitors = await fetchQuery(api.isup.monitors.list, {
    userId: user.convexUserId,
  });

  return Response.json({ monitors });
}

Notification System

Integration with Existing Infrastructure

┌─────────────────────────────────────────────────────────────────────┐
│                     Notification Flow                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   Incident Detected                                                  │
│         │                                                            │
│         ▼                                                            │
│   ┌──────────────┐                                                   │
│   │ Convex       │  Create incident record                          │
│   │ Mutation     │  Get alert contacts                              │
│   └──────┬───────┘                                                   │
│          │                                                            │
│          ▼                                                            │
│   ┌──────────────┐     ┌──────────────┐     ┌──────────────┐        │
│   │  Email       │     │  Slack       │     │  Webhook     │        │
│   │  (Resend)    │     │  API         │     │  HTTP POST   │        │
│   └──────────────┘     └──────────────┘     └──────────────┘        │
│          │                                                            │
│          │  Uses existing Resend setup from do-dev                   │
│          │                                                            │
│   ┌──────────────┐     ┌──────────────┐                              │
│   │  SMS         │     │  Discord     │                              │
│   │  (Twilio)    │     │  Webhook     │                              │
│   └──────────────┘     └──────────────┘                              │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Project Structure

Monorepo Addition

dodotdev/
├── apps/
│   ├── do-dev/           # Existing
│   └── isup-dev/         # NEW - IsUp application
│       ├── app/
│       ├── components/
│       ├── hooks/
│       ├── lib/
│       ├── package.json
│       └── next.config.mjs
├── packages/
│   ├── ui/               # Shared (existing)
│   ├── catalyst/         # Shared (existing)
│   └── convex/           # Add IsUp tables
├── tools/
│   ├── convex/           # Convex functions
│   │   └── convex/
│   │       ├── isup/     # NEW - IsUp functions
│   │       │   ├── monitors.ts
│   │       │   ├── incidents.ts
│   │       │   ├── statusPages.ts
│   │       │   └── alerts.ts
│   │       └── schema.ts # Add IsUp tables
│   └── workers/          # NEW - Cloudflare workers
│       └── isup-checker/
├── billing-dev/          # Existing
└── package.json

Development Phases

Phase 1: Foundation (Weeks 1-4)

  • Create apps/isup-dev Next.js app
  • Add IsUp tables to Convex schema
  • Set up WorkOS auth (copy from do-dev)
  • Create basic dashboard layout
  • Implement monitor CRUD (Convex)
  • Add billing-dev integration

Phase 2: Monitoring Core (Weeks 5-8)

  • Create Cloudflare Worker for checks
  • Set up Tinybird for check results
  • Implement HTTP/HTTPS monitoring
  • Build incident detection logic
  • Create email notifications (Resend)
  • Build monitor detail pages

Phase 3: MVP Launch (Weeks 9-12)

  • Add Slack integration
  • Create public status pages
  • Implement webhook notifications
  • Add response time graphs
  • Stripe checkout integration
  • Polish UI/UX

Phase 4: Growth (Post-Launch)

  • SSL/DNS monitoring
  • Team management
  • REST API
  • White-label status pages
  • Additional integrations

Configuration

Environment Variables

# apps/isup-dev/.env.local

# WorkOS (same as do-dev)
WORKOS_API_KEY=
WORKOS_CLIENT_ID=
WORKOS_COOKIE_PASSWORD=

# Convex
NEXT_PUBLIC_CONVEX_URL=
CONVEX_DEPLOY_KEY=

# Billing-dev API
BILLING_DEV_API_URL=https://billing.do.dev
BILLING_DEV_API_SECRET=

# Cloudflare
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_ACCOUNT_ID=

# Tinybird
TINYBIRD_API_URL=
TINYBIRD_API_KEY=

# Notifications
RESEND_API_KEY=
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=

# Monitoring
SENTRY_DSN=
NEXT_PUBLIC_POSTHOG_KEY=

Success Metrics

MetricMonth 1Month 3Month 6
Signups1005002,000
Active Monitors5005,00025,000
Paid Users1050200
MRR$100$500$2,000
Uptime99.9%99.9%99.95%

References

On this page