/** * The application (SPEC.md §9). * * One call assembles it. REST CRUD, the OpenAPI document, the API Explorer, the SDK * and the Schema Registry follow from the modules listed below; policies, revisions, * the audit log and change sets are not options at all, because an application * without them silently throws its history away. * * Nothing is declared yet — no model, no resource, no block. That is the starting * state on purpose: everything above is machinery, and what a project is about is the * first thing you add to `src/modules/content.ts`. * * `createApp()` hands back an application that has **not** been booted, because two * callers need one and neither should get the other's: `src/server.ts`, which serves * it, and `assemora.config.ts`, through which the `assemora` command boots it to * describe the real application rather than a parse of this source (ADR-0021). */ // assemora:if pages import { join } from 'node:path' // assemora:end import { auth } from '@assemora/auth' import { createMemoryAdapter, type DatabaseAdapter } from '@assemora/database' import { postgres } from '@assemora/database-postgres' // assemora:if pages import { pages } from '@assemora/pages' // assemora:end import { collections } from '@assemora/resources' import { type AssemoraApplication, assemora } from 'assemora' import manifest from '../package.json' with { type: 'json' } import { ENV_FILE } from './env.ts' import { content } from './modules/content.ts' /** * `.env`, read before anything asks what is in it. * * Node does not read `.env` on its own, and `assemora dev`, `assemora db:migrate` and * every other command reach this file through `assemora.config.ts`. Loading it here * rather than in `server.ts` is what makes one `.env` serve all of them — a migration * run against a different database than the server uses is a long afternoon. * * It never overwrites a variable the environment already has, so a container's * configuration still wins. */ try { process.loadEnvFile(ENV_FILE) } catch { // There is no .env, which is the ordinary case in a deployment. } /** * Where this project's data lives, or `undefined` when nowhere yet. * * Exported because `src/server.ts` asks the same question for a different reason: * the in-memory fallback is the one database a seed may create an account on * (see `src/seed.ts`), and both answers have to come from one place. */ export const databaseUrl = (): string | undefined => { const url = process.env.DATABASE_URL return url === undefined || url === '' ? undefined : url } /** * A real database when there is one, and a loud fallback when there is not. * * `pnpm create assemora demo && pnpm dev` has to show something working before * anybody has provisioned PostgreSQL, and a first run that dies on a connection error * loses people. The bargain is that it says so on every single boot: an in-memory * database is honest only while it is announcing itself. */ const database = (): DatabaseAdapter => { const url = databaseUrl() if (url !== undefined) return postgres({ url }) console.warn( 'DATABASE_URL is not set: this project is running on an in-memory database, and ' + 'everything in it disappears when the process restarts. Put a URL in .env, then ' + 'run `pnpm db:generate initial && pnpm db:migrate`.', ) return createMemoryAdapter() } export const createApp = (): AssemoraApplication => assemora({ database: database(), modules: [ // Users, sessions, roles, permissions and policies. Authorization denies by // default, so without this nobody — including you — could do anything at all. auth(), // assemora:if pages // The page builder. A block type reaches its palette by being listed here, and // by nothing else — `blocks: [Hero]` once `assemora make:block hero` has written // one. Until then the palette is empty, which is the truth about this project. pages(), // assemora:end // A collection is a resource made in Studio rather than in TypeScript, stored as // a definition and registered while the application boots (SPEC.md §37). It is // what makes the first thing in an empty project something a person can make // without opening an editor. collections(), content(), ], // Read from package.json so the three places that ask agree: the OpenAPI title, // the name the MCP server announces over the protocol, and what // `assemora.describe` tells an agent this project is. project: { name: manifest.name, version: manifest.version }, // assemora:if studio // Studio, served at /studio on this same origin, so its session cookie and CSRF // protection are first-party. The bundle comes from `@assemora/studio`, which // this project depends on and the framework deliberately does not. // // `languages` is which languages Studio offers to be *read* in — never the // languages of the content, which is `locales` (SPEC.md §131, ADR-0034). The bundle // speaks English, Ukrainian and Russian; this project offers English, because that // is the one a new project can be sure of, and a switcher offering languages nobody // in the room reads is furniture. Widen it — `['en', 'uk']` — or delete the line to // offer everything the bundle has. A language the bundle never had is a directory // of JSON: `studio: { languagePacks: './i18n' }`. studio: { languages: ['en'] }, // assemora:end // assemora:if mcp // An agent proposes; a person applies (SPEC.md §75). `mutations: 'direct'` is the // deliberate opt-out, and it belongs here where it can be seen. mcp: true, // assemora:end // assemora:if pages // The bundle `pnpm build` writes. Studio's canvas frames it at /preview, so what // an editor sees is this site's own components rather than an imitation of them // (SPEC.md §59). frontend: { root: join(import.meta.dirname, '../app/dist') }, // assemora:end })