// private beta · onboarding builders in waves request_access →
← Back to blog
Diagram of a block's declared contract — what it provides, what it requires, and the files it owns — composing with other blocks into an ordinary application repository

Composable Software: Inside Solo’s Block Architecture

How Solo uses versioned blocks, declared contracts, and file ownership to assemble a SaaS business without hiding its application code.

Composable software turns repeated application concerns into declared parts with stable contracts. For Solo, that means authentication, databases, payments, frontends, deployment, analytics, and email can be assembled while the product remains ordinary application code.

The obvious alternative is a template. Templates work well while every project has the same shape. Once one business needs Postgres without Stripe, or Postmark instead of SES, the generated result begins to diverge from its source.

Solo's answer is composable software: model each part as a block with a declared contract, then assemble a business from a selected set instead of one fixed template. Here is how that works and where the tradeoffs appear.

The problem with generating code

Ask an LLM to build a SaaS twice and you may get two plausible applications with different structure. Unless ownership and interfaces are part of the contract, a later tool cannot reliably tell which files belong to authentication, payments, or deployment.

Unconstrained generation does not guarantee stable seams. Each later change has to rediscover the structure or rewrite across concerns.

The alternative is to make the seams explicit before generation: declare what each part provides, what it requires, and which files it owns. Then assembly is a resolution problem, not a creative one.

What a block is

A block is a versioned block.yaml manifest plus optional templates or an executable action. Here is a trimmed excerpt from the Clerk authentication block:

apiVersion: solo.dev/v1
kind: Block
metadata:
  id: auth.clerk
  name: auth-clerk
  version: 0.2.0
spec:
  type: service
  category: auth
  slot: single
  provides:
    AUTH:
      type: capability
      interface: auth.AuthProvider
  requires:
    DATABASE_URL:
      type: env
      optional: true
  owns:
    - backend/solo/modules/auth_clerk.py
    - backend/solo/modules/auth_null.py
    - backend/solo/migrations/001_users.sql
  credentials:
    CLERK_SECRET_KEY:
      scope: secret

Four things are being declared, and each one buys something specific.

provides / requires is the assembly-time contract. AUTH is a capability this block satisfies; DATABASE_URL is an environment value it needs. A required binding that nothing satisfies makes the assembly invalid. Resolution fails before any file is written instead of failing later in front of a customer.

Note optional: true on the database. Clerk has a genuine frontend-only mode, so a business can install auth without a backend. Marking it required would make that shape unassemblable. Optionality here is a real product decision, not defensiveness.

owns lists the files this block renders, and every rendered path has exactly one owner. Declared ownership lets the system answer "who wrote this file?", detect a hand edit, and identify the paths associated with a block. A one-shot template cannot provide that history by itself.

category and slot make providers interchangeable. auth is a category; auth.clerk is one implementation. Another auth block satisfying the same interface can take the slot. That's what makes it composition rather than configuration.

credentials with a scope of public or secret tells provisioning what to collect and where it's allowed to appear. A secret-scoped credential never reaches the client bundle.

Three types, one purpose each

Type Owns Examples
infrastructure Runtime and deployment topology web-frontend-nextjs, backend-fastapi, database-postgres, cloud-deploy
service A provider seam an app calls auth-clerk, payments-stripe, analytics-posthog, email-postmark, email-ses
action Work the control plane performs content, seo, ads-plan, monitor, triage, backlog-groom

The distinction that matters most is this: infrastructure and services render files into your business; actions execute from the control plane. Actions operate against a business workdir. They are operations, not runtime code shipped with the application.

This boundary is why the assembled application can run without importing Solo's control plane. Marketing operations, health monitoring, and incident triage stay outside the business runtime. The repository contains the application and its ordinary stack.

From blocks to a real business

ListMyCar shows why the boundary matters. Its first repository came from an early Solo scaffold with a frontend, API, database migrations, authentication, billing integration, local development, and an AWS CDK definition. Those repeatable concerns arrived connected; the listing workflow remained product code that the team could edit with the tools it chose.

The production architecture stayed with the business. ListMyCar deploys from its own GitHub repository to infrastructure defined in that repository. Solo can coordinate the workflow, follow the released commit, and verify the result without becoming a dependency of the running application.

Diagram showing Solo turning goals into assembled ListMyCar software, then operating it through SEO, ads, deploy, billing, incident, and remediation loopsTyped blocks assemble the repeatable foundation. The product remains in a conventional repository, while recurring operations run around it.

That is the practical test for the architecture: composition should accelerate the parts that repeat without making the product unknowable or trapping the business in the composer. The ListMyCar case study follows that path from scaffold to measured operation.

Two layers of contract

provides/requires proves a dependency exists at assembly time. That is necessary but not sufficient. It says the auth block is installed, not that its implementation behaves correctly.

So there's a second layer: service ABCs. The auth.AuthProvider interface named in the manifest is a real abstract base class, and the installed implementation must actually satisfy it. The manifest proves presence; the ABC proves shape.

Alongside those, optional services ship null objects. Note auth_null.py in the owned paths above. Application code can call the interface consistently, while an absent provider uses a no-op implementation with explicit behavior. That avoids scattering provider checks across every caller and keeps the number of code paths manageable.

Scheduled and event-driven work

Actions declare how they activate: onCron, onEvent, onWebhook, onDemand, or onPlanApproval. Scheduled blocks declare their cadence in metadata, and Solo's daemon is the single evaluator for those schedules.

metadata:
  id: marketing.seo-publish
  role: marketing
spec:
  type: action
  execution:
    effect:
      mode: external
      reversibility: irreversible
  agent:
    activation: onDemand
    model: none
  autonomy:
    default: supervised

This excerpt from the SEO publish block declares both intent and consequence: it is supervised and its effect is external and irreversible. The manifest alone is not the security boundary. The publish path also requires a recorded approval and rechecks the approved content digest immediately before it writes. Monitoring can run without that gate because observation does not publish, deploy, or spend.

What the business knows about itself

An assembled business carries a .solo/checksums.json ledger for membership, ownership, and content hashes. It can identify which blocks are installed, which files each owns, and which files have changed since rendering.

That last point matters because you are meant to edit your code. When a hash diverges, the system can distinguish the rendered baseline from the file now on disk and apply its drift rules deliberately. Ownership is recorded instead of guessed.

Composite files, meaning outputs derived from the complete installed set, are regenerated output rather than a second source of truth. The manifests remain the source.

Where this is genuinely hard

Honest accounting, because composable architectures fail in predictable ways.

Categories are a commitment. Adding a service category needs an interface and runtime registration. A manifest naming a category with no consumer can install successfully yet have no effect. The extra work keeps categories tied to behavior rather than labels.

Ownership boundaries take judgment. Which block owns the FastAPI entry point when three blocks contribute routes? Solo's rule is that declaration follows the renderer: the block that writes the file owns it, while others contribute through composites. The engine enforces one owner per rendered path.

Composition is not free. A hand-written application can take any shape. A composed one can only express what its contracts allow. Solo uses that constraint for repeatable operating concerns while leaving the product itself as ordinary application code.

The catalog is finite. As of publication, the supported catalog includes Next.js and FastAPI, Postgres, Clerk, Stripe, PostHog, Postmark and SES, and AWS deployment. Broader stack choices and author-your-own custom blocks are roadmap items, not shipped features. If you need a provider outside the catalog, treat that as a current limitation.

Why it's built this way

The point of the block model is not elegance. It is that an autonomous system needs a dependable description of what it operates.

An agent fixing a failed deploy needs to know which block owns the deployment topology. A provider change needs a bounded set of files and interfaces. A health check needs to know what is installed for this business. Declared contracts make those questions deterministic instead of requiring the agent to infer the architecture each time.

Composable software here is not an architectural preference. It's the precondition for anything running your business without you. The companion guide, Own Your Code, shows how direct developer edits fit around those declared boundaries.

A real repo on a standard stack, assembled from declared parts. Keep the application code and infrastructure definition; let Solo operate around them. Join the waitlist →

Keep reading