Skip to main content

Project Descriptor

A project descriptor is the compiled representation of your foundry.config.ts. The create-foundry-app CLI compiles your TypeScript config into this JSON shape and submits it to POST /projects/deploy. You can also construct and submit the descriptor directly via the API.

How the reconciler uses the descriptor

The diagram below shows what happens after POST /projects/deploy receives a descriptor: the reconciler plans which resources to create, provisions them in dependency order (databases and files first, then the app), injects connection environment variables, and brings the project to Running. Re-submitting the same descriptor is an idempotent no-op.

POST /projects/deploy, dependency-ordered reconciliation
RUNNING all resources provisioned · env wired
DescriptorPOST /deployfan-out →postgresRunningfilesRunningDATABASE_URL · S3_* · FOUNDRY_API_TOKEN →appRunning
DescriptorPendingProvisioning / wiringRunningDATABASE_URLS3_*auth issuer

Reconciliation phases

  1. Submit. POST /projects/deploy accepts the descriptor and returns immediately. The reconciler picks it up asynchronously.
  2. Plan. The reconciler diffs the descriptor against any existing project state and decides which resources to create, update, or leave unchanged.
  3. Provision dependencies. Resources with no declared dependencies (typically postgres and files) are provisioned concurrently. The reconciler waits for each to reach Running before unblocking any resource that depends on it.
  4. Provision the app. Once all dependency resources are Running, the app container is provisioned.
  5. Wire connections. The reconciler injects connection environment variables (DATABASE_URL, S3_*, issuer_url, FOUNDRY_API_TOKEN) into the app and triggers a zero-downtime redeploy so the container starts with the full set of platform-managed variables.
  6. Running. The project reaches Running once all resources are provisioned and wired.
  7. Idempotent re-deploy. Submitting the same descriptor again produces a diff of zero changes. Every resource is already in the target state, so no work is done.

Top-level fields

FieldTypeRequiredDescription
namestringyesStable project identity. Changing the name creates a new project rather than updating the existing one.
resourcesarrayyesEvery declared resource. Exactly one must have kind: app.
dependenciesobjectnoMaps a resource logical_name to a list of logical_name values that must be Running before it is created.
organization_idUUIDnoScopes the project to an organization you belong to. Omit for a personal project.

Resources

Each entry in the resources array has three fields:

FieldTypeRequiredDescription
logical_namestringyesUnique name for this resource within the project. Used as the key in dependencies and as $name references inside other resources' specs.
kindstringyesOne of postgres, files, auth, app.
specobjectyesKind-specific configuration. Unknown keys are rejected.

Dependency ordering

The dependencies map expresses which resources must reach Running before a given resource is created. Independent resources provision concurrently.

{
"dependencies": {
"auth": ["db"],
"web": ["db", "auth"]
}
}

The engine also discovers implicit ordering edges from $logical_name references inside spec values: if the web spec contains "$db.service_id", the engine treats db as a dependency of web even if you did not list it explicitly.

When no dependencies key is present for a resource, the reconciler provisions it as soon as its own spec is valid. This means two independent postgres resources in the same descriptor provision in parallel.

postgres spec

Provisions a managed PostgreSQL service.

FieldTypeDefaultDescription
planstringtier-1Compute plan name (e.g. tier-1, tier-2).
storage_gbinteger25Data disk size in GB.
storage_tierstringplatform defaultStorage tier: standard or maxiops.
versionstringplatform defaultPostgreSQL major version (e.g. "17").
extensionsstring arraynoneExtensions to install (e.g. ["pgvector", "postgis"]).
default_databasestringdefaultdbName of the default database created during provisioning.

files spec

Provisions an S3-compatible object storage bucket.

FieldTypeDefaultDescription
quota_gbintegernoneSoft quota in GB (notification threshold).
quota_hard_gbintegernoneHard quota in GB (enforced ceiling).

auth spec

Attaches end-user authentication to the app resource. Auth does not create a standalone service; it enables an auth_configuration on the app it depends on. The auth resource must declare a dependency on the app resource (or the engine infers it from $ref notation).

FieldTypeDefaultDescription
providersstring array["password"]Enabled auth providers. Supported: password, google, github.
smtpobjectnoneSMTP configuration for magic-link email delivery. If absent, email-based flows are disabled.
idp_providersarraynoneSocial login provider configurations (client ID, secret).

When auth is enabled the reconciler records an issuer_url in the resource's outputs once the auth configuration is active.

app spec

Provisions a hosted application container.

FieldTypeRequiredDescription
imagestringyesContainer image reference (e.g. ghcr.io/myorg/myapp:latest).
portintegeryesThe container port the application listens on.
envobjectnoAdditional environment variables to inject alongside the platform-managed ones.
planstringnoCompute plan. Defaults to tier-1.
storage_gbintegernoPersistent storage for the app container.
custom_domainsstring arraynoCustom domains to attach to the app.

Complete example

The following descriptor declares a to-do application backed by PostgreSQL with the pgvector extension, end-user auth with password and Google login, and an application container.

{
"descriptor": {
"name": "my-todo-app",
"resources": [
{
"logical_name": "db",
"kind": "postgres",
"spec": {
"plan": "tier-2",
"storage_gb": 50,
"storage_tier": "maxiops",
"version": "17",
"extensions": ["pgvector"]
}
},
{
"logical_name": "uploads",
"kind": "files",
"spec": {
"quota_gb": 100
}
},
{
"logical_name": "auth",
"kind": "auth",
"spec": {
"providers": ["password", "google"],
"smtp": {
"host": "smtp.example.com",
"port": 587,
"username": "noreply@example.com",
"password": "smtp-secret"
},
"idp_providers": [
{
"provider": "google",
"client_id": "your-google-client-id",
"client_secret": "your-google-client-secret"
}
]
}
},
{
"logical_name": "web",
"kind": "app",
"spec": {
"image": "ghcr.io/myorg/todo:latest",
"port": 3000,
"plan": "tier-2",
"env": {
"NODE_ENV": "production",
"LOG_LEVEL": "info"
}
}
}
],
"dependencies": {
"auth": ["db"],
"web": ["db", "uploads", "auth"]
}
}
}

When this project reaches Running, the web container will have the following environment variables injected by the platform, in addition to those declared in env:

VariableSource
DATABASE_URLPostgreSQL connection string for db
S3_ENDPOINTFiles endpoint for uploads
S3_BUCKETBucket name for uploads
S3_ACCESS_KEYAccess key ID for uploads
S3_SECRETSecret access key for uploads
FOUNDRY_API_TOKENScoped platform token (read-only: services, files)
FOUNDRY_API_URLBase URL of the FoundryDB API

These variables are injected automatically. You do not declare them in env; doing so would override the platform-managed values with your literal string, which is almost never what you want.

Resource naming

The engine derives stable service names from the project name and the resource's logical_name. The same project-plus-logical-name always yields the same service name, which keeps re-deploys pointing at the same underlying service. Names are lowercased, sanitized to [a-z0-9-], and capped at 60 characters.

Next steps