Skip to content
Reference

loor.json

The single declarative manifest behind every Loor project — services, ports, domains, databases, and mobile apps. Studio and Vibe Chat keep it in sync, but it's plain JSON you can read, diff, and edit.

What it is

Every Loor project has one loor.json at its root. It's the source of truth for what your project is made of: each app (service), the ports they run on, the domains they answer to, the databases they need, and — for mobile — how they get built and signed. The same file drives local dev, branch previews, and production. You rarely write it by hand: Studio writes it as you click, and Vibe Chat edits it when you ask ("add Redis"). But it's yours — version-controlled, reviewable, and editable.

The shape

{
  "version": "1",
  "project": {
    "name": "my-app",
    "type": "fullstack",          // fullstack · api · client · monorepo
    "defaultPreview": "client"    // which service the web preview opens
  },
  "services": {
    "client": { /* … */ },
    "api":    { /* … */ },
    "mobile": { /* … */ }         // expo / react-native (see below)
  },
  "resources": {
    "mongodb": { "enabled": true },
    "redis":   { "enabled": false }
  },
  "scripts": { /* optional named commands */ }
}

Top level: version, a project block, a services map, and optional resources (databases) and scripts. Everything else lives under a service.

project

project.name is required. project.type is a hint (fullstack, api, client, monorepo). project.defaultPreview names the service the web preview opens by default — it must match a service key.

services

Each service is keyed by name and points at a folder. A service is one runnable thing — a web app, an API, a worker, or a mobile app. Only type is required; Loor fills in sensible defaults and auto-detects the type from your repo when you leave it off.

"api": {
  "type": "nodejs",               // pin it, or let Loor detect from the repo
  "root": "apps/api",             // folder, relative to the project root
  "port": 5001,                   // dev / preview port
  "dev": "pnpm dev",              // command Studio runs for live dev
  "build": {
    "command": "pnpm build",
    "output": "dist",             // static output dir (for static/SPA types)
    "installCommand": "pnpm install"
  },
  "start": { "command": "node dist/index.js" },  // production entrypoint
  "domain": "api.example.com",    // string, or the structured form below
  "env": { "NODE_ENV": "production" },
  "secrets": ["JWT_SECRET"]       // names only — values live encrypted in Studio
}

Service types

The recognized type values:

static · react · astro · nextjs · vue · angular
nodejs · nestjs · python · go · turbo · docker
custom · worker · expo · react-native

Web/static types (react, astro, nextjs, vue, angular, static) build to an output dir and serve over a domain. Server types (nodejs, nestjs, python, go, docker, custom) run a long-lived process via start.command. worker runs with no inbound domain. expo / react-native are mobile — they build to native artifacts on a Mac host instead of serving HTTP (see below).

Ports & domains

port is the dev/preview port. domain attaches a hostname in production — either a plain string, or a structured object when you want Loor to manage the DNS record and TLS on a Cloudflare zone.

// simple — a plain hostname
"domain": "app.example.com"

// structured — let Loor manage DNS + TLS on a Cloudflare zone
"domain": {
  "mode": "auto",               // auto = Loor creates the DNS record
  "subdomain": "app",
  "zoneId": "<cloudflare-zone-id>",
  "proxied": true               // orange-cloud / WAF
}

Env & secrets

Non-sensitive config goes in services.<name>.env as a flat map. secrets lists the env keys that are sensitive — their values are set in the Studio sidebar (or the CLI) and stored encrypted at rest. Never paste a real secret value into loor.json; it's committed to git, and Studio masks anything listed in secrets. For the env vars themselves — service URLs, managed-DB connections, generated secrets — see .env.example.loor.

resources — databases & cache

Backing services are toggles under resources. Flip one on and Loor provisions it, manages it, and injects its connection string (MONGODB_URL, REDIS_URL) into your services. No host, port, or password to wire by hand.

"resources": {
  "mongodb": { "enabled": true },   // provisioned + MONGODB_URL injected
  "redis":   { "enabled": true }    // provisioned + REDIS_URL injected
}

See Databases & cache for versions and backups.

scripts

Named one-off commands you can run from Studio — seeds, migrations, maintenance tasks. Each has a command, an optional service it runs inside, and an optional description.

"scripts": {
  "seed":    { "command": "pnpm db:seed", "service": "api", "description": "Seed dev data" },
  "migrate": { "command": "pnpm migrate" }
}

Mobile apps

An expo (or react-native) service is a mobile app. Mobile services are domainless — no domain, no HTTP port; they use metroPort for the live Metro dev server and build to native .ipa / .apk / .aab artifacts on a Mac host. The optional inner mobile block describes bundle identifiers and per-profile build settings — the profiles mirror EAS so migrating off it is mechanical.

"mobile": {
  "type": "expo",                 // or "react-native"
  "root": "apps/mobile",
  "metroPort": 8081,              // live Metro dev server (no domain — mobile is domainless)
  "dev": "pnpm dev",
  "mobile": {
    "platforms": ["ios", "android"],
    "ios":     { "bundleIdentifier": "com.acme.app", "deploymentTarget": "16.0", "appleTeamId": "ABCDE12345" },
    "android": { "package": "com.acme.app", "minSdkVersion": 24, "targetSdkVersion": 34 },
    "build": {
      "profiles": {
        "development": { "ios": { "buildConfiguration": "Debug",   "simulator": true },          "android": { "buildType": "apk" } },
        "preview":     { "ios": { "buildConfiguration": "Release", "distribution": "internal" }, "android": { "buildType": "apk" } },
        "production":  { "ios": { "buildConfiguration": "Release", "distribution": "store" },     "android": { "buildType": "aab" } }
      }
    }
  }
}

The whole mobile block is optional — with just { "type": "expo", "root": "apps/mobile" } Loor defaults to both platforms and the three standard profiles. Signing material (App Store Connect API keys, certificates, Android keystores) is not in loor.json — it's stored encrypted in Studio. The full walkthrough — simulator preview, build profiles, and signing — is in Mobile apps.

How Studio uses it

Studio reads loor.json to build the project sidebar, decide what to provision, run your dev servers, plan the deploy, and — for mobile — drive builds on the Mac host. When Vibe Chat changes the project ("add MongoDB", "add a mobile app"), it edits this file and shows you the diff before applying. Config is validated on every change, so a typo'd service type or a domain on a mobile service is caught before it ships.