Kinotic
Kinotic OS

Contributing

How to contribute to the Kinotic OS project.

Overview

We welcome contributions to Kinotic OS. This guide covers the repository structure, build process, and contribution workflow.

Repository Structure

DirectoryDescription
kinotic-core/Java/Kotlin backend (Spring Boot) — RPC gateway, service registry, authentication
kinotic-management-api/Domain model and management API services — application, project, and cluster management
kinotic-app-api/The app-api services an application's runtimes and users call, hosted by the app server — the service directory registration of an application's services
kinotic-ee/Enterprise Features, licensed under the Kinotic Enterprise License — the Azure Key Vault secret storage backend
kinotic-js/TypeScript SDK workspace (Bun) — @kinotic-ai/core, @kinotic-ai/persistence, and related packages
kinotic-frontend/Vue.js UI workspace (pnpm) — apps/portal (the Kinotic OS dashboard), apps/system (the platform-operator console), packages/common (shared UI code)
website/Documentation site (Docus/Nuxt)
deployment/Helm charts, Docker Compose, Terraform configurations

Building

Java/Kotlin Backend

./gradlew build

TypeScript SDK

cd kinotic-js
bun install
bun run build

Website

cd website
bun install
bun run dev

Local development environment

There are two ways to run Kinotic locally, for two different people. Evaluating Kinotic, the whole platform runs in Docker and you use the portal the management server serves on http://localhost:9090. Developing Kinotic, only the backing services run in Docker, you run the org, system and app servers from your IDE, and the portal runs from source on vite's http://localhost:5173. Anything involving GitHub — linking an organization, creating projects, deployments on push — needs an ngrok tunnel and a GitHub App of your own with either setup, because the App names your tunnel in its callback and webhook URLs, and a GitHub App has only one of each.

Prerequisites

  • Docker
  • For development, Bun, pnpm, JDK 25 (the shared IntelliJ run configurations use graalvm-ce-25), and IntelliJ's EnvFile plugin, which loads .env.local into the server's environment
  • For GitHub, Bun and pnpm, and an ngrok account with its free static domain, e.g. you.ngrok-free.dev, and the ngrok CLI signed in to it (ngrok config add-authtoken <token>)

Evaluating Kinotic

cd deployment/docker-compose
docker compose up -d

That brings up the whole platform from published images:

ServiceAddressWhat it is
kinotic-elasticsearchlocalhost:9200Everything the platform stores
kinotic-migration—Creates the indices and seeds the platform rows, then exits; the servers wait for it
kinotic-server-managementlocalhost:9090, localhost:58503The portal build baked into the image on 9090, and the org API and STOMP gateway on 58503
kinotic-server-systemlocalhost:58504The API the system console and vm-manager nodes call
kinotic-server-applocalhost:58505Every application's API, each at <organizationId>--<applicationId>.localhost:58505
kinotic-gen-schemas—Publishes a set of sample entity definitions through the management server, then exits
Kibanalocalhost:5601Browses Elasticsearch
OpenTelemetry collector, Tempo, Loki, Mimir, Grafanalocalhost:3000 (Grafana)The servers' traces, logs and metrics

Open http://localhost:9090 and sign in as kinotic@kinotic.local with the password kinotic. That user belongs to the test-org organization, which the stack fills with sample data: the migration runs with the fixtures profile (evaluation.env), which seeds applications, projects, members and pending invites, and kinotic-gen-schemas creates the ecommerce and healthcare applications with published entity definitions and records. Signing up instead creates an organization of your own, which starts empty.

Screens with sample dataScreens that stay empty
Applications, their projects, members and settings; the organization's members and pending invites; entity definitions and their records in ecommerce and healthcare; your profileDeployments, and the deployment sections of the application and project overviews; jobs; observability and traces; the organization's GitHub link

The empty screens fill only from what a deployed project produces: a deployment starts when a push reaches the management server from GitHub, runs as workloads on a vm-manager node, and sends the telemetry the observability screens read. Using Kinotic locally for real, rather than walking its screens, takes your own tunnel and GitHub App and a vm-manager node to run the workloads.

Email is off in the compose servers, so a sign-up's verification link is in the management server's log (docker compose logs -f kinotic-server-management). docker compose pull refreshes the images, and docker compose down -v wipes Elasticsearch; the next up migrates and seeds it again. deployment/docker-compose/README.md lists the other combinations of the stack.

Developing Kinotic

Start only the backing services, including the observability stack the shared run configuration exports traces to:

cd deployment/docker-compose
docker compose -f compose.elasticsearch.yml -f compose.kinotic-migration.yml -f compose-otel.yml up -d

Run the servers from the ManagementServerApplication, SystemServerApplication and AppServerApplication run configurations. They run the development profile, the system server with local as well, and load .env.local from the repository root, which must exist (touch .env.local). The management server's development profile sends email through the platform's Azure Communication Services; add KINOTIC_DOMAIN_EMAIL_ENABLED=false to .env.local to log verification links instead. When you change the migration, run KinoticMigrationApplication from the IDE instead of the container.

Then run the portal from source:

cd kinotic-frontend
pnpm install
pnpm dev

The portal on http://localhost:5173 talks to the management server on localhost:58503, and the links the management server's development profile builds point at it.

Your tunnel and GitHub App

Register the App once, naming your ngrok domain:

bun dev-tools/github-app/dev-github-app.ts create --domain you.ngrok-free.dev

A browser page opens on GitHub with the App filled in; confirm it. The App is named kinotic-dev-<your username> unless you pass --name (App names are unique across GitHub), and it belongs to your account unless you pass --org <organization>. It is private, so it can be installed only on the account that owns it, and that account is where the projects you create get their repositories. It carries:

SettingValue
Webhook URLhttps://<domain>/api/github/webhook, subscribed to push
Callback URLshttps://<domain>/api/github/install/callback first, then the github-platform sign-in callback under /api/auth/org/login/social/callback/, which every flow through the registration returns to
Request user authorization (OAuth) during installationon
Repository permissionsAdministration: write, Contents: write, Metadata: read
Account permissionsEmail addresses: read

The script writes the override files, an application.yml for each of the org and system servers under ~/.kinotic/dev-environment/kinotic-server-management and ~/.kinotic/dev-environment/kinotic-server-system, and beside them github-app.json with every credential GitHub returned. It then runs sync-es, below, and restarts the compose kinotic-server-management and kinotic-server-system containers that are running so they load the App; servers run from the IDE need restarting by hand.

The App lives on GitHub, so it outlasts every wipe of your stack; register it once. What a fresh migration loses is its link to Kinotic: the migration seeds the github-platform sign-in row with the kinotic-ai App's client id, and an organization's link to your App's installation is a row in Elasticsearch. After every fresh migration (docker compose down -v, or a new cluster), restore both:

bun dev-tools/github-app/dev-github-app.ts sync-es

sync-es points the sign-in row at your App's client id and, once the App is installed, links its installation to the test-org organization (--organization-id for another), which is what linking GitHub in the portal does. It reads the App from github-app.json and asks GitHub for the installation with the App's own key, so nothing is registered or installed again.

To register the App again, for a new domain say, delete the old one on GitHub (Settings → Developer settings → GitHub Apps) and run create with --force.

With the servers running, from compose or the IDE, start the tunnel and the portal from source behind it; the tunnel fronts vite's dev server, whose proxy reaches the management server, since the portal on 9090 is static files alone:

ngrok http --url=you.ngrok-free.dev 5173
cd kinotic-frontend && pnpm install && pnpm dev:tunnel

pnpm dev:tunnel makes the portal call its own origin, and its dev server forwards /api, (REST and the STOMP WebSocket at /api/v1), /.well-known and /mcp to the management server on port 58503, so the portal, the API, OAuth callbacks, the GitHub webhook and the MCP endpoint all answer on https://you.ngrok-free.dev.

Open that origin and sign in as kinotic@kinotic.local with the password kinotic. The first time, link GitHub when creating a project: GitHub installs your App, returns to /api/github/install/callback, which forwards to the portal's /github/install/callback, and the management server verifies the installation belongs to you before storing it; after a wipe, sync-es restores that link. Projects you create get a repository under the account the App is installed on, and pushes to them reach your management server through the webhook.

Running workloads on your machine

A deployment runs its workloads on a vm-manager node. For local development that is the BOXLITE provider on your own machine, which runs each workload as a micro VM: macOS on Apple silicon, or Linux with /dev/kvm. The node works beside the servers from compose or the IDE alike, since the system server answers on localhost:58504 in both and runs the development profile.

cd kinotic-js/workspace
bun install && bun run build   # the node resolves the workspace packages through their dist
cd packages/vm-manager
bun run dev

.env.development, which Bun loads for bun run dev, supplies the rest: the node id dev-node-1, the SYSTEM machine identity the development migration seeds, and compose's Loki and OTLP collector for workload logs, traces and metrics. The node registers with the system server and shows ONLINE in the system console under Platform → Worker nodes. Guests reach the org and app servers at 192.168.127.254, boxlite's alias for the host, which the system server's development profile sets as kinotic.systemApi.deployment.managementServer.host and appServer.host.

A push to a project's repository then deploys it: a sync workload checks the commit out, installs it and syncs its entities, and a runtime workload starts each microservice. The first deployment pulls kinoticai/workload-runner, which takes about a minute. A workload's logs are written under /tmp/kinotic-vm-logs/<workload id> and shipped to Loki, where the logs on a project's deployment page read them. A deployment that fails records why on its job run, under Jobs in the portal.

  • The system server's development profile marks a project's UIs deployed without serving them; microservices run for real.
  • The node keeps its image cache in ~/.boxlite, which one process can hold at a time: stop the node before running the vm-manager tests, or give the tests their own BOXLITE_HOME.
  • The evaluator stack's sample nodes are marked unreachable on the system server's first heartbeat pass, so deployments land on your node.

The override files

The files under ~/.kinotic/dev-environment are ordinary Spring configuration, one for each server that uses the App, and the one place your tunnel and App reach the servers. Each server's application-development.yml imports its own, the management server's:

spring:
  config:
    import: optional:file:${user.home}/.kinotic/dev-environment/kinotic-server-management/application.yml

and the system server's, under kinotic-server-system. The compose servers load the same files: compose.kinotic-servers.yml mounts each server's directory read-only at /workspace/config, and Spring reads ./config/application.yml from the server's working directory ahead of every file packaged in the jar, so the management server's file replaces the compose profile's http://localhost:9090 origin too. Change a file and restart its server, however you run it.

create writes the management server's like this. The system server's holds the managementApi block alone: the system server keeps its own origins, and uses the App to mint the tokens deployments fetch their repositories with.

# The github-platform sign-in row's OAuth client secret
KINOTIC_AKV_GITHUB_PLATFORM: "<client secret>"
kinotic:
  domain:
    portalBaseUrl: https://you.ngrok-free.dev
  managementApi:
    apiBaseUrl: https://you.ngrok-free.dev
    github:
      appId: "1234567"
      appSlug: kinotic-dev-you
      webhookSecret: "<generated by GitHub>"
      appPrivateKey: |
        -----BEGIN RSA PRIVATE KEY-----
        ...
        -----END RSA PRIVATE KEY-----

In a server run from the IDE an imported file takes precedence over the file that imports it, so these values replace the development profile's http://localhost:5173 origin and its placeholder App key; application-local.yml (the system server's local profile comes after development) and environment variables still win over it. Without the files the servers run on their localhost origins, from the IDE or compose. In development, named secrets such as the sign-in row's github-platform resolve from KINOTIC_AKV_<NAME> properties, which is why the client secret can sit at the top of the management server's file rather than in an environment variable.

You can edit the files by hand, for example to put your own values in place of what create wrote. The origins must match the App's URLs on GitHub, so moving to another domain means editing both the management server's file and the App's callback and webhook URLs, or registering a new App.

On Linux the compose servers read their files as their image's own user, so create makes each application.yml readable to other users there; elsewhere they and github-app.json are yours alone. If compose starts before create has run, Docker creates the directories it mounts under ~/.kinotic/dev-environment owned by root, and create says so and prints the chown that fixes it.

Publishing UIs against Azure

The development profile marks every published UI ready without uploading or serving it. To exercise the real path — sites published into a sites account and served by the Cloudflare sites Worker — point the server at a subscription and a Cloudflare zone of your own.

Create what the server cannot create itself, once:

az login
cd deployment/terraform/azure/dev
terraform init
terraform apply   # environment = "local" in terraform.tfvars; the cloudflare_* variables in local.auto.tfvars
terraform output -raw application_local_yml > ../../../../kinotic-server-system/src/main/resources/application-local.yml
terraform output -raw app_server_application_local_yml > ../../../../kinotic-server-app/src/main/resources/application-local.yml

That is a resource group with the sites storage account, the sites Worker and settings on the kinotic.io zone serving <app>--<ui>.<org>.<environment>.kinotic.io from the account, and a service principal for the server with the roles it needs: Storage Blob Data Contributor on the account, with which it signs each site's upload and removal URLs, and Contributor on the email service, so it sends mail too. The written files are the git-ignored local profiles: the system server's turns the site provisioner on, names the domain and the account, and turns on the Cloudflare provisioner that creates each organization's record under your subdomain; the app server's names the sites domain, from whose pages alone it admits a sign-in, so both servers run with development,local. The environment is yours alone: it is your label under the zone, so two developers sharing one would collide. The zone's id and account id, a token terraform applies the zone with, and a token holding DNS edit on the zone alone go in local.auto.tfvars.

The principal's AZURE_CLIENT_ID, AZURE_CLIENT_SECRET and AZURE_TENANT_ID, and the DNS token as KINOTIC_DOMAIN_PUBLISHEDSITES_CLOUDFLARE_APITOKEN, are written to .env.local at the repository root, which git ignores, as a commented block after a blank line. When the file exists its other lines are kept and the block a previous apply wrote is replaced, or appended if absent. Run the server with that file in its environment; DefaultAzureCredential takes those variables before anything else, so the server signs as the principal whatever az login is signed in as. A role assignment takes a minute or two to become visible; a publish before that fails with AuthorizationFailed on the workload, and the next deploy of the project succeeds.

Run the system server, which publishes the UIs, with both profiles (the IDE's SystemServerApplication run configuration does):

SPRING_PROFILES_ACTIVE=development,local

A developer machine is outside any platform VNet, so the publish workload reaches the sites account over its public endpoint, which the workload's egress allowlist names.

Before involving the server, publish a site against your subscription from a test. It does what a deployment does for a site under <environment>.<zone>, uploading through an upload URL and reading back through the edge what it published, and checks that the URLs it issues act on one site's directory alone, so a step Azure rejects fails naming the call:

./gradlew :kinotic-system-api:test --tests '*AzureProvisioningIntegrationTest*'

The module's test task puts .env.local in the test's environment, and the test reads your application-local.yml; without either it skips. What it publishes is left in place, so a second run is quick, and terraform destroy removes it with the account.

Then deploy a project that contains a UI; the deployment publishes it into the sites account and the site serves at https://<app>--<ui>.<org>.<environment>.<zone> as soon as its files are up and the organization's record, created at sign-up or by this publish, has its certificate, which shows as the deployment turning from provisioning to ready within moments.

terraform destroy removes the resource group with the account, the Worker and its routes. The organizations' records the server created under your subdomain stay in the zone until removed by hand.

Testing

Java/Kotlin

./gradlew test

TypeScript (Vitest)

cd kinotic-js
bun test

Submitting Changes

  1. Fork the repository and create a feature branch from develop
  2. Make your changes with clear, descriptive commit messages
  3. Ensure all tests pass before submitting
  4. Submit a pull request against the develop branch
  5. Accept the Contributor License Agreement on your first pull request by commenting I have read the Kinotic CLA Document and I hereby sign the CLA

Licensing

Everything outside kinotic-ee/ is licensed under the Elastic License 2.0 with the Kinotic Additional Limitations; kinotic-ee/ holds the Enterprise Features, licensed under the Kinotic Enterprise License. The root LICENSE.md routes each part of the repository to its license. Any interface in an Elastic-licensed package with an ee segment, such as org.kinotic.domain.api.ee.services.SecretStorageBackend, is an Enterprise Extension Point: its implementations for production use are Enterprise Features and belong in kinotic-ee/. Enterprise License describes what production use of them requires.

The CLA grants Minds Ignited the right to license your contribution under any of these licenses, including as part of an Enterprise Feature; you keep the copyright in it.

Copyright © 2026