Directory structure
Where each kind of file lives, and what ohne does with it.
In ohne, where a file sits decides what it is. A file in collections/ is a collection, a file in api/ is a route, and a file in dashboard/pages/ is a dashboard page. You never register or import these files. ohne finds them by their location.
A new project has none of these directories. You create each one the first time you need it. A project that uses all of them looks like this:
my-app/
├── api/ HTTP routes
├── blocks/ reusable content shapes
├── boot/ code that runs once at startup
├── collections/ your data model
├── commands/ your own ohne commands
├── dashboard/
│ ├── boot/ code that runs once when the dashboard loads
│ ├── components/ UI code your pages share
│ ├── pages/ dashboard pages
│ └── tsconfig.json
├── fields/ custom field types
├── messages/ translatable strings
├── middleware/
│ └── global/ middleware that runs on every request
├── migrations/ data moves for schema changes
├── roles/ who may do what
├── skills/ what the assistant can do on request
├── flows/ how the assistant routes a request
├── .data/ the local database
├── .ohne/ generated types
├── .env local environment variables
├── ohne.config.ts
├── package.json
└── tsconfig.jsonThe rest of this page walks through them, one part of the app at a time.
Data
collections/- one file per collection. The file names the collection, socollections/Posts.tsisPosts, and ohne creates its table for you.fields/- custom field types.fields/sku.tsdefines askutype, which any collection uses asfield('sku').blocks/- blocks, reusable content shapes like a hero or a quote.blocks/Hero.tsdefinesHero.migrations/- migrations. Each file describes one change that has to carry data along, like renaming a field.
API
api/- routes. The file's path is the URL, and a suffix sets the method:api/posts.get.tsisGET /posts.middleware/- middleware, functions that run before a route's handler. A file undermiddleware/global/runs on every request. Any other file runs only on the routes that select it.
Dashboard
Everything under dashboard/ is browser code. ohne serves it to the browser as it is, so treat the whole directory as public. Server code lives outside it, and the two talk over HTTP.
dashboard/pages/- pages. The file's path is the route:dashboard/pages/posts.tsis/posts.dashboard/boot/- dashboard boot files. Each one runs once in the browser, before the first page renders.dashboard/components/- the UI code your pages share. This one is only a habit. ohne readspages/andboot/, and the rest ofdashboard/is yours to arrange. Pages import it through theapp/prefix.dashboard/tsconfig.json- type-checks the browser code against DOM types, apart from your Node code.
Across the app
boot/- boot files. Each one runs once at startup, before the server opens. This is where you register hooks.roles/- roles, one per file.roles/editor.tsis theeditorrole, a named set of permissions you give to users.skills/- skills, one per file.skills/translate-items.tsis thetranslate-itemsskill, a prompt the dashboard assistant runs when someone asks for it.flows/- flows, one per file.flows/raid-officer.tsis theraid-officerflow, a graph the assistant walks: it decides what a request needs, then runs the matching skill or prompt.commands/- commands, one per file.commands/seed.tsaddsohne seed, for work outside a request.messages/- message catalogs, the strings your app translates. One JSON file per language:messages/en.json,messages/de.json.
Files ohne writes
You do not create these, and the scaffold's .gitignore keeps them out of git:
.ohne/- the types ohne generates from your project. They are what makes your queries, routes, and messages typed in the editor. You never edit or import them..data/- the local database, a SQLite file that ohne creates on the first boot.
.env is also ignored, but you write that one yourself. It holds local environment variables, such as secrets.
The files at the root are covered in Installation.
Renaming a directory
The names above are defaults. If you would rather keep your routes in routes/, set dirs in your config:
import { defineConfig } from 'ohnejs';
export default defineConfig({
layers: ['ohnejs/base'],
dirs: {
api: 'routes',
},
});Layers share the structure
A layer is an ohne project you build on, and it uses these same directories. The ohnejs/base layer in your config has its own collections/, api/, and dashboard/, which is where your app's users, sign-in, and dashboard come from.
When your project and a layer define the same thing, like a route with the same URL or a collection with the same name, yours wins.
Next, Your first app fills some of these directories: a collection, two routes, and a dashboard page.