Reference

Batteries

Three services the platform runs so a small app does not have to. They are HTTP APIs on batteries.saasie.io, reached with a token that identifies your Saasie — not you.

Getting the credential

Declare the battery in saasie.yaml and the platform injects its address and token. Declare nothing and you get no credential at all — which is the difference between a platform that hands out secrets and one that answers requests.

BATTERIES

Three services the platform runs so your app does not have to: analytics,
outbound email, and inference. All are HTTP APIs, reached with a token that
identifies your Saasie.

Neither is on by default. Declare what you use, and the platform injects the
address and the token for it — a blueprint that declares nothing gets no
credential at all.

  version: 1

  batteries:
    analytics:
    mail:
    inference:

  services:
    backend:
      dockerfile: Dockerfile
      context: .
      port: 3000
      health:
        path: /health
      env:
        SAASIE_ANALYTICS_URL:   { battery: analytics, property: url }
        SAASIE_ANALYTICS_TOKEN: { battery: analytics, property: token }
        SAASIE_MAIL_URL:        { battery: mail, property: url }
        SAASIE_MAIL_TOKEN:      { battery: mail, property: token }
        SAASIE_MAIL_FROM:       { battery: mail, property: from }
        SAASIE_INFERENCE_URL:   { battery: inference, property: url }
        SAASIE_INFERENCE_TOKEN: { battery: inference, property: token }

The env var names are yours; only the references are fixed. "token" is the
same value whichever battery you ask — it identifies the Saasie, not the
battery — so one variable is enough if you would rather.

Send it as: Authorization: Bearer <token>

The URLs point at the batteries service, which is a different host from the
one this CLI talks to. Read them from the environment; do not hardcode them.

ANALYTICS

Register a type before you log events of it. Registration is idempotent, so
call it at startup and do not track whether you have.

  POST $SAASIE_ANALYTICS_URL/types
    { "name": "signup", "properties": { "plan": "string" } }

  POST $SAASIE_ANALYTICS_URL/events
    { "events": [ { "type": "signup", "properties": { "plan": "pro" } } ] }

  POST $SAASIE_ANALYTICS_URL/query
    { "type": "signup", "since": 1754438400000, "interval": "day",
      "groupBy": "plan" }

An event whose type is not registered is refused, and so is a property the
type did not declare. That is the point: a typo in an event name is caught at
the call that made it, rather than becoming a second dimension that reads as
zero forever while the real one looks like it dropped.

  "at" is optional — epoch milliseconds or ISO — and defaults to now.
  Property values may be strings, numbers or booleans; all are stored as
  strings.
  Up to 100 events per request. A batch is stored whole or not at all.

MAIL — SENDING

  POST $SAASIE_MAIL_URL/send
    { "to": "someone@example.com", "subject": "Your receipt",
      "text": "Thanks!" }

It is sent from $SAASIE_MAIL_FROM — your Saasie's own address, assigned by
the platform. You cannot choose it. It looks like:

  acme-x7k2p9@saasie.email

The random half is not decoration. Names can be claimed again after a Saasie
is deleted, and without it the next "acme" would receive mail meant for the
last one.

MAIL — RECEIVING

There is none. The address sends only; mail sent *to* it is discarded, and
nothing bounces. If your app needs to hear back from someone, put a form in
the app rather than asking them to reply.

INFERENCE

Two models, named by what they are for rather than by what they are:

  POST $SAASIE_INFERENCE_URL/generate
    { "prompt": "Summarise this in one line: ..." }

  POST $SAASIE_INFERENCE_URL/embed
    { "input": ["first passage", "second passage"] }

  GET  $SAASIE_INFERENCE_URL/models

"saasie/fast" generates and is the default for /generate. "saasie/embed"
embeds and is the default for /embed. You do not get to choose the model
behind either, and that is deliberate: it means we can move to a better one
without breaking your app.

  Send "messages" instead of "prompt" for a conversation:
    [ { "role": "user", "content": "..." } ]
  "maxTokens" caps the answer. Ask for more than the platform allows and you
  get the platform's ceiling — check "finishReason": "length" tells you the
  answer was cut off.
  /embed takes one string or a batch. Vectors come back in the order you
  sent them; pair them with your own ids by position. Read "dimensions"
  rather than hardcoding it.

Nothing you send is stored. We record how many tokens a call used, and never
the prompt, the answer, or the vectors.

WHAT YOU GET, AND WHAT STOPS YOU

  5 emails per day, to any address
  100,000 events per day
  events kept 90 days
  50,000 generation tokens per day
  500,000 embedding tokens per day

Every count resets at midnight UTC. Past any of them, the API answers 429 and
says when it resets — handle that rather than retrying, because retrying will
not help until the next day.

Inference is counted in tokens rather than in calls, because that is what it
costs. Roughly: a token is about four characters, and a request reserves its
whole "maxTokens" up front — so a large "maxTokens" you do not use still
takes up room in the day's budget. Ask for what you need.

All of it is covered by the subscription. That is what the caps are for: a
capped service can be included, an uncapped one has to be metered.

THE TOKEN

It identifies your Saasie and nothing else — it cannot claim a name, read
another Saasie's data, or act on your account. Treat it as a secret anyway:
anything holding it can send mail as you, and there are only five a day.

It is not printed by any command and not shown in any console, because the
only thing that needs it is your container. It stops working while the Saasie
is suspended, and starts again when it resumes.

Read what your app has been doing:

  saasie app <name> analytics types
  saasie app <name> analytics count --type signup --since 30d
  saasie app <name> mail

The same text, on your machine: saasie docs batteries. The machine-readable contract is openapi.json.