Learn-skills.dev saas-controller

install
source · Clone the upstream repo
git clone https://github.com/NeverSight/learn-skills.dev
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/afterthought/saas-controller/saas-controller" ~/.claude/skills/neversight-learn-skills-dev-saas-controller && rm -rf "$T"
manifest: data/skills-md/afterthought/saas-controller/saas-controller/SKILL.md
source content

SaaS Controller

Multi-cloud service orchestration for devenv. Declarative service definitions with pluggable providers. Manages local dev (

sc up
with Tailscale HTTPS) and cloud deployment (
sc deploy
).

Architecture

Providers own the full service lifecycle. Each provider generates its own docker-compose stack with a Tailscale sidecar for HTTPS on the tailnet.

┌──────────────────────────────────────────────────────┐
│                  saas-controller                      │
│                                                       │
│  ┌────────────────────────────────────────────────┐  │
│  │              Providers (WHAT)                   │  │
│  │  Each provider owns up() + deploy()            │  │
│  ├────────────────────────────────────────────────┤  │
│  │  zuplo          │ API gateway + docs portal    │  │
│  │  docker-compose │ Generic compose stacks       │  │
│  │  [your own]     │ via externalProviders        │  │
│  └────────────────────────────────────────────────┘  │
│                                                       │
│  sc up topology (per service):                        │
│  ┌──────────────────────────────────────────┐        │
│  │ docker-compose stack                      │        │
│  │  ┌───────────┐  ┌──────────────────────┐ │        │
│  │  │ tailscale │  │ app container(s)     │ │        │
│  │  │ sidecar   │◀─│ network_mode:        │ │        │
│  │  │           │  │   service:tailscale  │ │        │
│  │  │ HTTPS:443 │  │ PORT=3000            │ │        │
│  │  └───────────┘  └──────────────────────┘ │        │
│  │  URL: https://sc-<slug>-<service>.<tailnet> │     │
│  └──────────────────────────────────────────┘        │
└──────────────────────────────────────────────────────┘

Import

# devenv.yaml
imports:
  - github:afterthought/saas-controller

Or as a flake input:

# flake.nix
inputs.saas-controller.url = "github:afterthought/saas-controller";
# In your devenv module:
imports = [ inputs.saas-controller.outPath ];

Example A: Minimal Service

A hello-world service running locally with Tailscale HTTPS:

# devenv.nix
{ pkgs, lib, config, ... }:
{
  imports = [ /* saas-controller module */ ];

  saas-controller.services.hello-world = {
    enable = true;
    provider = "hello-world";
    providerConfig = {
      path = "examples/hello-world";  # dir with server.mjs + Dockerfile
    };
    environments = {
      local.enable = true;
    };
  };
}
sc up              # Starts compose stack with tailscale sidecar
                   # Prints: https://sc-<slug>-hello-world.<tailnet>:443

Example B: Zuplo Gateway with Secrets

A Zuplo API gateway with secretspec profiles, multiple environments, and secret management:

{ pkgs, lib, config, ... }:
{
  imports = [ /* saas-controller module */ ];

  saas-controller.services.my-gateway = {
    enable = true;
    displayName = "My API Gateway";
    provider = "zuplo";
    providerConfig = {
      project = "my-gateway";       # Zuplo project name
      account = "my-account";       # Zuplo account
      path = "services/my-gateway"; # Path to zuplo project in repo
    };

    environments = {
      local.enable = true;
      production.enable = true;
      preview.enable = true;
    };

    # Secret management
    secretspec = {
      auth.provider = "client-myorg";  # SecretSpec provider alias
      auth.saToken = "client-myorg";   # 1Password SA token alias
      environments = {
        local = {
          serviceProfiles = [ "tailscale" ];
          # → validates TS_CLIENT_SECRET, TS_CLIENT_ID
        };
        production = {
          serviceProfiles = [ "zuplo-backend" ];
          # → validates zuplo secrets for production
        };
      };
      tags = [ "tailscale" "zuplo" ]; # For filtered checking
    };
  };
}
sc up                              # Start locally with tailscale HTTPS
sc deploy my-gateway -e production  # Deploy to production
sc check-secrets --tag tailscale   # Validate tailscale secrets

Secret Profiles

Secrets are managed in two layers: controller-level profile definitions and per-service composition.

Controller Level: Define profiles

saas-controller.secretProfiles = {
  tailscale = {
    TS_CLIENT_SECRET = {
      description = "Tailscale OAuth client secret";
      required = true;
      providers = [ "saas-controller" ];
    };
    TS_CLIENT_ID = {
      description = "Tailscale OAuth client ID";
      required = false;
      providers = [ "saas-controller" ];
    };
  };
  my-api-keys = {
    API_KEY = {
      description = "External API key";
      providers = [ "saas-controller" ];
    };
  };
};

Service Level: Compose profiles per environment

services.my-service.secretspec = {
  auth.provider = "client-myorg";      # SecretSpec provider alias
  auth.saToken = "client-myorg";       # 1Password SA token alias
  environments = {
    local = {
      serviceProfiles = [ "tailscale" ];
      # Only tailscale secrets needed locally
    };
    production = {
      serviceProfiles = [ "my-api-keys" ];
      # API keys needed for production deployment
    };
  };
  tags = [ "tailscale" ];            # For sc check-secrets --tag
};

Provider Auto-Export

Providers can declare

secretProfiles
in their implementation. These are automatically merged into
saas-controller.secretProfiles
. When a service uses a provider, that provider's profiles are auto-included — no manual wiring needed.

For example, the

zuplo
provider exports
zuplo
and
zudoku
profiles. Any service with
provider = "zuplo"
automatically gets those profiles available.

Checking Secrets

sc check-secrets                         # Check all services
sc check-secrets --tag tailscale         # Only tailscale-tagged services
sc check-secrets --service my-gateway    # Specific service
sc secret-status                         # Show secret-to-service mapping table

CLI Reference

# Local development
sc up                                    # Start all local services
sc up my-gateway                         # Start specific service

# Deployment
sc deploy                                # Deploy all to production (default)
sc deploy --environment production       # Deploy all to production
sc deploy my-gateway -e preview          # Deploy specific service to preview
sc undeploy my-gateway                   # Remove persistent service

# Secret management
sc check-secrets                         # Validate all service secrets
sc check-secrets --tag tailscale         # Filter by tag
sc check-secrets --service my-gateway    # Filter by service
sc secret-status                         # Secret-to-service mapping table

# Secret reconciliation
sc setup-env production                  # Check all secrets for production
sc diff-secrets local production         # Compare secrets between environments
sc reconcile-secrets                     # Show all secrets across all environments
sc reconcile-secrets -e production       # Show secrets for one environment

# Other
sc help                                  # Show help
provision-projects                       # One-time project setup

Task Integration

# sc up is also available as a devenv task
devenv tasks run saas:up

# Deploy with environment via task input
DEVENV_TASK_INPUT='{"environment": "production"}' devenv tasks run saas-deploy:my-gateway

Provider Summary

ProviderproviderConfig keyssc up?Auto-exported profiles
zuplo
project
,
account
,
path
Yes
zuplo
,
zudoku
docker-compose
path
,
composeFile
(opt),
tailscale
(opt)
Yes(none)

For detailed provider documentation: read

references/provider-reference.md

Extensibility

Register custom providers:

saas-controller.externalProviders.my-provider = ./providers/my-provider.nix;

See EXTENDING.md for provider authoring guide and template.

Tailscale Setup

sc up
requires one-time Tailscale setup (ACL tags, OAuth client). See
references/tailscale-setup.md
for step-by-step instructions.

SA Token Provider Setup

Services with

secretspec.auth.saToken
need the
sa-tokens
secretspec provider alias configured. The secretspec.toml is auto-generated from service configs at nix eval time — developers only configure the provider backend once.

One-time setup per machine:

secretspec config provider add sa-tokens "keyring://"   # macOS Keychain
secretspec config provider add sa-tokens "env://"       # Environment variables (CI)

Naming:

saToken = "client-willdan"
maps to
OP_SA_CLIENT_WILLDAN
.

Verify:

secretspec config provider list

Deeper Questions

For questions not covered here, use DeepWiki MCP:

ask_question("afterthought/saas-controller", "<your question>")

Or read the source at

github:afterthought/saas-controller
.