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
| Dependency | Purpose | Source |
|---|---|---|
| WorkOS AuthKit | Authentication, SSO | do-dev |
| Convex | Real-time database, functions | @workspace/convex |
| billing-dev API | Subscriptions, entitlements, usage | billing-dev |
| @workspace/ui | Radix UI components | packages/ui |
| @do/catalyst | Tailwind components | packages/catalyst |
| Resend | Email notifications | Shared |
| Sentry | Error tracking | Shared |
| PostHog | Analytics | Shared |
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
| Endpoint | Purpose |
|---|---|
POST /checkout | Create Stripe checkout session |
POST /portal | Open Stripe customer portal |
GET /entitlement?userId= | Get plan limits & features |
GET /subscription?userId= | Get subscription status |
POST /usage/record | Record API/monitor usage |
POST /cancel | Cancel subscription |
IsUp Plans (billing-dev products)
Add to billing-dev products table:
| Slug | Type | Price | Included Limits |
|---|---|---|---|
isup-hobby | base_plan | $0 | 10 monitors, 5min interval, 3mo retention |
isup-pro | base_plan | $9/mo | 50 monitors, 1min interval, 12mo retention |
isup-team | base_plan | $29/mo | 200 monitors, 30sec interval, 24mo retention |
isup-enterprise | base_plan | Custom | Unlimited, 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.jsonDevelopment Phases
Phase 1: Foundation (Weeks 1-4)
- Create
apps/isup-devNext.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
| Metric | Month 1 | Month 3 | Month 6 |
|---|---|---|---|
| Signups | 100 | 500 | 2,000 |
| Active Monitors | 500 | 5,000 | 25,000 |
| Paid Users | 10 | 50 | 200 |
| MRR | $100 | $500 | $2,000 |
| Uptime | 99.9% | 99.9% | 99.95% |