Skip to content

dot-do/saaskit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

117 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

SaaSkit

Idea β†’ Profitable SaaS. Wantrepreneur β†’ Entrepreneur.

import { SaaS } from 'saaskit'

export default () => (
  <SaaS name="YourIdea">
    {$ => {
      $.nouns({ Customer: { name: 'string', email: 'string' } })
      $.verbs({ Customer: { notify: $ => $.api.emails.send({ to: $.record.email }) } })
    }}
  </SaaS>
)

// That's it. You now have:
// βœ“ App    β†’ yourapp.saas.dev (React admin dashboard)
// βœ“ API    β†’ api.yourapp.saas.dev (REST + GraphQL + WebSocket)
// βœ“ Site   β†’ yourapp.io.sb (Landing page)
// βœ“ Docs   β†’ docs.yourapp.io.sb (API reference)
// βœ“ CLI    β†’ npx yourapp customers notify <id>
// βœ“ MCP    β†’ Works with Claude, ChatGPT, Cursor
// βœ“ SDK    β†’ npm install yourapp (TypeScript client)
// βœ“ Zapier β†’ Your app is an integration
// βœ“ Stripe β†’ Subscriptions flowing

The Problem

You have an idea. You've probably had it for months.

But between your idea and a running business stands:

β–‘ Authentication        β–‘ Landing page         β–‘ Legal entity
β–‘ Authorization         β–‘ Documentation        β–‘ Business banking
β–‘ User management       β–‘ API reference        β–‘ Compliance certs
β–‘ Billing integration   β–‘ SDK generation       β–‘ Marketing
β–‘ Subscription logic    β–‘ CLI tooling          β–‘ Sales
β–‘ Admin dashboard       β–‘ Webhook system       β–‘ Support
β–‘ CRUD interfaces       β–‘ Rate limiting        β–‘ Onboarding
β–‘ Audit logging         β–‘ Error handling       β–‘ Analytics

That's 6-12 months of work. And you haven't even started on your actual idea.

SaaSkit deletes all of it.


The Solution

Define your business in Nouns and Verbs. Everything else derives.

Nouns: What Exists

$.nouns({
  Customer: {
    name: 'string',
    email: 'string',
    plan: '->Plan',              // Customer owns link to Plan
    orders: ['<-Order'],         // Orders link back to Customer
  },

  Order: {
    items: ['->Product'],
    total: 'number',
    status: 'pending | paid | shipped | delivered',
    customer: '->Customer',
  },

  Product: {
    name: 'string',
    price: 'number',
    inventory: 'number',
  },
})

Verbs: What Happens

$.verbs({
  Order: {
    create: $ => $.db.Order.create($.input),

    pay: async $ => {
      await $.api.stripe.charges.create({
        amount: $.record.total,
        customer: $.record.customer.stripeId,
      })
      return $.db.Order.update($.id, { status: 'paid' })
    },

    ship: async $ => {
      const label = await $.api.shippo.labels.create({
        to: $.record.customer.address,
        parcel: $.record.items,
      })
      await $.api.emails.send({
        to: $.record.customer.email,
        subject: 'Your order has shipped!',
        body: `Track it here: ${label.tracking_url}`,
      })
      return $.db.Order.update($.id, { status: 'shipped', trackingUrl: label.tracking_url })
    },
  },

  Product: {
    restock: $ => $.db.Product.update($.id, {
      inventory: $.record.inventory + $.input.quantity
    }),
  },
})

Events: React to Changes

$.on.Order.paid(async (order, $) => {
  await $.api.slack.send('#sales', `New order: $${order.total}`)
  await $.db.Metric.increment('revenue', order.total)
})

$.on.Product.updated(async (product, $) => {
  if (product.inventory < 10) {
    await $.api.emails.send({
      to: 'inventory@yourcompany.com',
      subject: `Low stock: ${product.name}`,
    })
  }
})

Schedules: Recurring Tasks

$.every.day.at6am(async $ => {
  const unpaid = await $.db.Order.find({
    status: 'pending',
    createdAt: { $lt: $.time.daysAgo(3) },
  })

  for (const order of unpaid) {
    await $.api.emails.send({
      to: order.customer.email,
      subject: 'Complete your order',
      body: await $.ai`Write a friendly reminder about their pending order`,
    })
  }
})

$.every.Monday.at9am(async $ => {
  const revenue = await $.db.Metric.sum('revenue', { period: 'week' })
  await $.api.slack.send('#metrics', `Weekly revenue: $${revenue}`)
})

The $ Context

The $ is your handle to everything. No configuration. No setup. Just use it.

interface $ {
  // Database
  db: {
    [Noun]: {
      create, get, update, delete, list, find,
      search,           // Full-text search
      semanticSearch,   // AI-powered semantic search
    }
  }

  // AI
  ai`prompt`            // AI-generated content
  agents: {             // Named AI workers
    [name]: { run }
  }

  // Human-in-the-loop
  human: {
    approve,            // Requires human approval
    ask,                // Ask human a question
    review,             // Human reviews content
  }

  // Workflows
  send(event, data)     // Fire and forget (durable)
  do(action, data)      // Wait for result (durable)

  // 9000+ Integrations
  api: {
    emails,             // Emails.do
    texts,              // Texts.do
    calls,              // Calls.do
    stripe,             // Payments.do
    slack,              // via APIs.do
    hubspot,            // via APIs.do
    // ...etc
  }

  // Context
  input                 // Verb input payload
  record                // Current noun instance
  id                    // Record ID
  user                  // Authenticated user
  org                   // Current organization
  env                   // Environment variables
  time                  // Time helpers
}

AI Built In

Generative Content

$.verbs({
  BlogPost: {
    draft: async $ => {
      const content = await $.ai`
        Write a blog post about: ${$.input.topic}

        Tone: Professional but approachable
        Length: 800-1000 words
        Include: Introduction, 3 main points, conclusion
      `
      return $.db.BlogPost.create({ ...$.input, content, status: 'draft' })
    },
  },

  Customer: {
    summarize: $ => $.ai`
      Summarize this customer's activity:

      Orders: ${$.record.orders.length}
      Total spent: $${$.record.orders.reduce((s, o) => s + o.total, 0)}
      Last order: ${$.record.orders[0]?.createdAt}

      Write 2-3 sentences highlighting key insights.
    `,
  },
})

AI Agents

$.agent('support', {
  instructions: `
    You are a helpful customer support agent.
    Be friendly and professional.
    If you can't resolve an issue, escalate to a human.
  `,
  tools: ['getCustomer', 'getOrder', 'refundOrder', 'escalate'],
})

$.verbs({
  Ticket: {
    resolve: $ => $.agents.support.run({
      ticket: $.record,
      customer: $.record.customer,
      history: $.record.messages,
    }),
  },
})

Human-in-the-Loop

$.verbs({
  Refund: {
    process: async $ => {
      // Large refunds require approval
      if ($.record.amount > 500) {
        const approved = await $.human.approve(
          `Approve refund of $${$.record.amount} for ${$.record.customer.name}?`
        )
        if (!approved) return $.db.Refund.update($.id, { status: 'rejected' })
      }

      await $.api.stripe.refunds.create({ charge: $.record.chargeId })
      return $.db.Refund.update($.id, { status: 'processed' })
    },
  },
})

The Cascade

From <SaaS nouns={} verbs={} />, everything derives:

App

Full React admin dashboard. Automatically.

yourapp.saas.dev
β”œβ”€β”€ /dashboard          β†’ Overview with metrics
β”œβ”€β”€ /customers          β†’ CRUD list/create/edit/show
β”œβ”€β”€ /orders             β†’ CRUD + status workflow + actions
β”œβ”€β”€ /products           β†’ CRUD + inventory management
β”œβ”€β”€ /settings           β†’ Organization settings
β”œβ”€β”€ /team               β†’ User management
β”œβ”€β”€ /billing            β†’ Stripe customer portal
β”œβ”€β”€ /api-keys           β†’ Key management
└── /webhooks           β†’ Webhook configuration

API

REST + GraphQL + WebSocket. Automatically.

api.yourapp.saas.dev

REST:
GET    /customers              β†’ list
POST   /customers              β†’ create
GET    /customers/:id          β†’ get
PUT    /customers/:id          β†’ update
DELETE /customers/:id          β†’ delete
POST   /orders/:id/pay         β†’ verb
POST   /orders/:id/ship        β†’ verb

GraphQL:
query { customers { id name orders { total } } }
mutation { payOrder(id: "...") { status } }
subscription { orderCreated { id total } }

Site

Landing page with your messaging. Automatically.

yourapp.io.sb
β”œβ”€β”€ Hero section (from StoryBrand)
β”œβ”€β”€ Features (from your Nouns + Verbs)
β”œβ”€β”€ Pricing (from your Plans)
β”œβ”€β”€ Testimonials (when you have them)
└── CTA β†’ Sign up

Docs

API reference + guides. Automatically.

docs.yourapp.io.sb
β”œβ”€β”€ Getting Started
β”‚   β”œβ”€β”€ Quick Start
β”‚   β”œβ”€β”€ Authentication
β”‚   └── Your First Request
β”œβ”€β”€ API Reference
β”‚   β”œβ”€β”€ Customers
β”‚   β”œβ”€β”€ Orders
β”‚   └── Products
β”œβ”€β”€ Webhooks
β”‚   └── Event Reference
└── SDKs
    β”œβ”€β”€ JavaScript
    β”œβ”€β”€ Python
    └── Go

CLI

Command-line interface. Automatically.

$ npm install -g yourapp-cli

$ yourapp login
$ yourapp customers list
$ yourapp orders create --customer cus_123 --items prod_456
$ yourapp orders pay ord_789
$ yourapp orders ship ord_789

MCP

Your SaaS works with AI tools. Automatically.

// In Claude, ChatGPT, Cursor, etc:
"Use yourapp to list my customers"
"Create a new order for customer John Smith"
"Ship order #789"

SDK

Type-safe client libraries. Automatically.

import { YourApp } from 'yourapp'

const client = new YourApp({ apiKey: '...' })

// Typed CRUD
const customer = await client.customers.create({
  name: 'John Smith',
  email: 'john@example.com.ai',
})

// Typed verbs
await client.orders.pay('ord_123')
await client.orders.ship('ord_123')

// Real-time subscriptions
client.orders.on('created', (order) => {
  console.log('New order:', order.total)
})

Zapier

Your SaaS is an integration. Automatically.

Your app auto-publishes to Zapier with:
β”œβ”€β”€ Triggers (every event)
β”‚   β”œβ”€β”€ Order Created
β”‚   β”œβ”€β”€ Order Paid
β”‚   β”œβ”€β”€ Customer Created
β”‚   └── ...
β”œβ”€β”€ Actions (every verb)
β”‚   β”œβ”€β”€ Create Order
β”‚   β”œβ”€β”€ Pay Order
β”‚   β”œβ”€β”€ Ship Order
β”‚   └── ...
└── Searches (every noun)
    β”œβ”€β”€ Find Customer
    β”œβ”€β”€ Find Order
    └── ...

Full Example: RecruitKit

A complete SaaS for recruiters, in one file:

import { SaaS } from 'saaskit'

export default () => (
  <SaaS name="RecruitKit">
    {$ => {
      // ════════════════════════════════════════════════════════════════
      // NOUNS: Define what exists
      // ════════════════════════════════════════════════════════════════

      $.nouns({
        Recruiter: {
          name: 'string',
          email: 'string',
          company: '->Company',
          searches: ['<-Search'],
        },

        Candidate: {
          name: 'string',
          email: 'string',
          skills: ['~>Skill'],        // Fuzzy match to skill taxonomy
          resume: 'markdown?',
          linkedin: 'url?',
          matches: ['<-Match'],
        },

        Search: {
          title: 'string',
          criteria: 'string',
          recruiter: '->Recruiter',
          matches: ['->Match'],
          status: 'active | paused | closed',
        },

        Match: {
          score: 'number',
          status: 'new | shortlisted | contacted | interviewing | rejected | hired',
          search: '->Search',
          candidate: '->Candidate',
          notes: 'markdown?',
        },
      })

      // ════════════════════════════════════════════════════════════════
      // VERBS: Define what happens
      // ════════════════════════════════════════════════════════════════

      $.verbs({
        Search: {
          create: $ => $.db.Search.create({ ...$.input, status: 'active' }),

          run: async $ => {
            // AI-powered candidate matching
            const candidates = await $.db.Candidate.semanticSearch($.record.criteria)

            const matches = await Promise.all(
              candidates.slice(0, 50).map(async candidate => {
                const score = await $.ai`
                  Score 0-100 how well this candidate matches the search criteria.
                  Return ONLY a number.

                  Search: ${$.record.title}
                  Criteria: ${$.record.criteria}

                  Candidate: ${candidate.name}
                  Skills: ${candidate.skills.join(', ')}
                  Resume: ${candidate.resume || 'Not provided'}
                `

                return $.db.Match.create({
                  search: $.record.id,
                  candidate: candidate.id,
                  score: parseInt(score),
                  status: 'new',
                })
              })
            )

            await $.send('Search.completed', {
              search: $.record,
              matchCount: matches.length,
            })

            return matches
          },

          pause: $ => $.db.Search.update($.id, { status: 'paused' }),
          close: $ => $.db.Search.update($.id, { status: 'closed' }),
        },

        Match: {
          shortlist: $ => $.db.Match.update($.id, { status: 'shortlisted' }),

          contact: async $ => {
            const candidate = await $.record.candidate
            const search = await $.record.search

            // AI writes personalized outreach
            const email = await $.ai`
              Write a recruiting outreach email.

              Candidate: ${candidate.name}
              Role: ${search.title}
              Their skills: ${candidate.skills.join(', ')}

              Be warm and professional. Mention specific skills.
              Keep it under 200 words.
            `

            await $.api.emails.send({
              to: candidate.email,
              subject: `Exciting opportunity: ${search.title}`,
              body: email,
            })

            return $.db.Match.update($.id, { status: 'contacted' })
          },

          schedule: async $ => {
            // Human picks interview time
            const time = await $.human.ask(
              `When should we schedule the interview with ${$.record.candidate.name}?`
            )

            await $.api.calendar.create({
              title: `Interview: ${$.record.candidate.name}`,
              time,
              attendees: [$.record.candidate.email, $.record.search.recruiter.email],
            })

            return $.db.Match.update($.id, { status: 'interviewing' })
          },

          reject: async $ => {
            // Requires confirmation for candidates in interview stage
            if ($.record.status === 'interviewing') {
              const confirmed = await $.human.approve(
                `Reject ${$.record.candidate.name}? They are in the interview stage.`
              )
              if (!confirmed) return
            }

            await $.api.emails.send({
              to: $.record.candidate.email,
              subject: `Update on your application`,
              body: await $.ai`
                Write a kind rejection email for ${$.record.candidate.name}
                who applied for ${$.record.search.title}.
                Be respectful and encouraging.
              `,
            })

            return $.db.Match.update($.id, { status: 'rejected' })
          },

          hire: async $ => {
            await $.db.Match.update($.id, { status: 'hired' })
            await $.db.Search.update($.record.search.id, { status: 'closed' })
            await $.db.Metric.increment('hires')

            // Celebrate!
            await $.api.slack.send('#wins', `
              πŸŽ‰ ${$.record.candidate.name} hired for ${$.record.search.title}!
            `)
          },
        },

        Candidate: {
          enrich: async $ => {
            // Pull data from Apollo
            const data = await $.api.apollo.people.enrich({
              email: $.record.email,
            })

            return $.db.Candidate.update($.id, {
              name: data.name || $.record.name,
              linkedin: data.linkedin_url || $.record.linkedin,
              skills: data.skills || $.record.skills,
            })
          },

          import: async $ => {
            // Import from LinkedIn URL
            const data = await $.api.proxycurl.linkedin.get({
              url: $.input.linkedinUrl,
            })

            return $.db.Candidate.create({
              name: data.full_name,
              email: data.email,
              linkedin: $.input.linkedinUrl,
              skills: data.skills?.map(s => s.name) || [],
              resume: data.summary,
            })
          },
        },
      })

      // ════════════════════════════════════════════════════════════════
      // EVENTS: React to changes
      // ════════════════════════════════════════════════════════════════

      $.on.Match.created(async (match, $) => {
        if (match.score >= 80) {
          await $.api.emails.send({
            to: match.search.recruiter.email,
            subject: `High-quality match: ${match.candidate.name} (${match.score}/100)`,
            body: `${match.candidate.name} scored ${match.score}/100 for "${match.search.title}"`,
          })
        }
      })

      $.on.Search.completed(async ({ search, matchCount }, $) => {
        await $.api.emails.send({
          to: search.recruiter.email,
          subject: `Search complete: ${matchCount} candidates found`,
          body: `Your search "${search.title}" found ${matchCount} candidates. Review them now.`,
        })
      })

      $.on.Match.hired(async (match, $) => {
        // Send offer letter
        await $.api.emails.send({
          to: match.candidate.email,
          subject: `Offer Letter: ${match.search.title}`,
          body: await $.ai`
            Write a professional offer letter for ${match.candidate.name}
            for the role of ${match.search.title}.
          `,
        })
      })

      // ════════════════════════════════════════════════════════════════
      // SCHEDULES: Recurring tasks
      // ════════════════════════════════════════════════════════════════

      $.every.day.at9am(async $ => {
        // Remind recruiters about stale matches
        const stale = await $.db.Match.find({
          status: 'new',
          createdAt: { $lt: $.time.daysAgo(7) },
        })

        const byRecruiter = groupBy(stale, m => m.search.recruiter.id)

        for (const [recruiterId, matches] of Object.entries(byRecruiter)) {
          const recruiter = await $.db.Recruiter.get(recruiterId)
          await $.api.emails.send({
            to: recruiter.email,
            subject: `${matches.length} candidates awaiting review`,
            body: `You have ${matches.length} candidates that haven't been reviewed in 7+ days.`,
          })
        }
      })

      $.every.Monday.at9am(async $ => {
        // Weekly metrics
        const hired = await $.db.Match.count({
          status: 'hired',
          updatedAt: { $gte: $.time.daysAgo(7) },
        })
        const contacted = await $.db.Match.count({
          status: 'contacted',
          updatedAt: { $gte: $.time.daysAgo(7) },
        })

        await $.api.slack.send('#metrics', `
          πŸ“Š Weekly Recruiting Report
          β€’ Hired: ${hired}
          β€’ Contacted: ${contacted}
          β€’ Conversion: ${((hired / contacted) * 100).toFixed(1)}%
        `)
      })

      // ════════════════════════════════════════════════════════════════
      // AGENTS: AI workers
      // ════════════════════════════════════════════════════════════════

      $.agent('sourcer', {
        instructions: `
          You are a technical recruiter specialized in sourcing candidates.

          Your job:
          1. Understand the role requirements
          2. Find candidates that match
          3. Score candidates based on fit
          4. Explain why each candidate is a good match

          Be thorough but prioritize quality over quantity.
          A great match is better than 10 mediocre ones.
        `,
        tools: ['searchCandidates', 'enrichCandidate', 'scoreMatch'],
      })

      $.agent('outreach', {
        instructions: `
          You are a recruiting outreach specialist.

          Your job:
          1. Write personalized emails to candidates
          2. Reference their specific skills and experience
          3. Explain why this role is a good fit for them
          4. Be warm but professional

          Never be generic. Every email should feel personal.
        `,
        tools: ['getCandidate', 'getSearch', 'sendEmail'],
      })

      // ════════════════════════════════════════════════════════════════
      // INTEGRATIONS
      // ════════════════════════════════════════════════════════════════

      $.integrate('apollo', { apiKey: $.env.APOLLO_API_KEY })
      $.integrate('proxycurl', { apiKey: $.env.PROXYCURL_API_KEY })
      $.integrate('slack', { webhook: $.env.SLACK_WEBHOOK })
      $.integrate('calendar', { provider: 'google', oauth: true })
    }}
  </SaaS>
)

That's a complete recruiting SaaS. App, API, landing page, docs, CLI, SDK, webhooks, billing β€” all derived from 250 lines of business logic.


The Ecosystem

SaaSkit is part of Startups.Studio:

Startups.Studio
β”œβ”€β”€ Startups.Studio/StartupBuilder   β†’ Build startups in minutes
β”œβ”€β”€ Startups.Studio/ServiceBuilder   β†’ AI-delivered Services-as-Software
└── Startups.Studio/SalesBuilder     β†’ Mark (Marketing) + Sally (Sales)

SaaSkit (npm: saaskit)
β”œβ”€β”€ SaaS.Dev         β†’ SaaSkit-as-a-Service (managed hosting)
└── SaaS.Studio      β†’ Manage your SaaS (MRR, customers, revenue)

Services.Studio      β†’ Managed Services-as-Software

Platform.do          β†’ The PaaS for Business-as-Code
β”œβ”€β”€ Agents.do        β†’ AI workers with tools
β”œβ”€β”€ Database.do      β†’ Schema-first database
β”œβ”€β”€ Functions.do     β†’ Serverless execution
β”œβ”€β”€ Workflows.do     β†’ Durable state machines
β”œβ”€β”€ Payments.do      β†’ Stripe Connect
β”œβ”€β”€ Bank.Accounts.do β†’ Business banking + cards
β”œβ”€β”€ Emails.do        β†’ Transactional + marketing
β”œβ”€β”€ Texts.do         β†’ SMS
β”œβ”€β”€ Calls.do         β†’ Voice + AI receptionist
└── ...              β†’ 20+ more primitives

Builder.Domains
β”œβ”€β”€ *.io.sb          β†’ Free app domains
β”œβ”€β”€ *.app.net.ai     β†’ Free app domains
β”œβ”€β”€ *.api.net.ai     β†’ Free API domains
└── Any .tld         β†’ Register any domain

Entry Points

Where What
Startups.New "Vibe code" your startup. AI conversation β†’ running business in minutes.
Startup.Games Gamify your journey from idea to one-person unicorn.
SaaS.Dev Deploy your SaaSkit app. Managed hosting.
SaaS.Studio Manage your SaaS. MRR, ARR, churn, customers, revenue.

Requirements

Runtime Environment

Requirement Version Notes
Node.js >=18.0.0 Required for native fetch, ES modules
React ^18.0.0 or ^19.0.0 Peer dependency for UI components

Environment Variables

SaaSkit uses environment variables for authentication and configuration:

Variable Required Description
SAASKIT_API_TOKEN Yes (production) API token for Platform.do services (Emails.do, Texts.do, etc.). Development mode uses a fallback token with a console warning.
SAAS_API_KEY For deployment Required when running saaskit deploy to authenticate with SaaS.Dev hosting.
NODE_ENV No Set to test to suppress development warnings.

Integration-specific variables (optional, used via $.env):

  • STRIPE_API_KEY / STRIPE_SECRET_KEY - For Stripe payments
  • APOLLO_API_KEY - For Apollo enrichment
  • SLACK_WEBHOOK - For Slack notifications
  • Any custom variables your app needs via $.env.YOUR_VAR

Platform Services

SaaSkit connects to the Platform.do ecosystem for backend services:

Service URL Purpose
Emails.do emails.do Transactional & marketing email
Texts.do texts.do SMS messaging
Calls.do calls.do Voice calls & AI receptionist
Payments.do payments.do Stripe integration
APIs.do apis.do 9000+ third-party integrations

Note: These services require a SAASKIT_API_TOKEN. During development, a fallback token is used automatically.

Optional Dependencies

{
  "optionalDependencies": {
    "@dotdo/react": "^0.1.0"  // Enhanced React integration
  }
}

Development Setup

  1. Create a .env file from the example:

    cp .env.example .env
  2. Add your API token:

    # .env
    SAASKIT_API_TOKEN=your-token-here
  3. For deployment, also add:

    SAAS_API_KEY=your-deployment-key

Get Started

Install

npm install saaskit

Create

// app.tsx
import { SaaS } from 'saaskit'

export default () => (
  <SaaS name="MyApp">
    {$ => {
      $.nouns({
        Todo: { title: 'string', done: 'boolean' },
      })
      $.verbs({
        Todo: {
          create: $ => $.db.Todo.create($.input),
          complete: $ => $.db.Todo.update($.id, { done: true }),
        },
      })
    }}
  </SaaS>
)

Deploy

npx saaskit deploy

Done

Your SaaS is live at myapp.saas.dev.


The Promise

You have an idea. Stop letting infrastructure stand between you and your dream.

Define your business in Nouns and Verbs. We generate everything else.

Idea β†’ Profitable SaaS. Wantrepreneur β†’ Entrepreneur.


Start Building β†’


Architecture

SaaSkit sits at the generation layer - it takes DSL definitions (Nouns + Verbs) and generates code that uses mdxui components.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      mdxui (interfaces)                          β”‚
β”‚   SiteComponents, AppComponents, AdminComponents, DataProvider   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              ↓ implements
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              @mdxui/beacon + @mdxui/cockpit (templates)          β”‚
β”‚   Site sections, App dashboards, Auth flows                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              ↓ generates to
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚               β˜… saaskit (generation) ← YOU ARE HERE              β”‚
β”‚   DSL (Nouns + Verbs) β†’ App, API, Site, Docs, CLI, SDK, MCP      β”‚
β”‚   Generates code that uses @mdxui components                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              ↓ optionally uses
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    @dotdo/react (platform)                       β”‚
β”‚   TanStack DB binding for real-time data sync                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

What SaaSkit Generates

Output Using
App React components (raw + @mdxui/cockpit)
Site @mdxui/beacon components
API Self-contained REST/GraphQL/WebSocket
Docs MDX documentation
CLI Command-line interface
SDK TypeScript/Python/Go clients
MCP Model Context Protocol server

Key Principle

SaaSkit is generation, not runtime. It produces code that uses mdxui abstractions. The generated code is self-contained and can be ejected.

When @dotdo/react is available, SaaSkit wires generated code to real backends with durable workflows.

About

Full-stack SaaS admin framework with dotdo primitives and TanStack ecosystem

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages