# Start here

This page takes you from nothing to a unit running inside your own application. It is in two parts,
and they are very different sizes.

**Part 1 takes a minute.** You write a file and call it.

**Part 2 takes an afternoon.** You wire the SDK into your own application so units run in your JVM,
against your classes and your data. Most of that time is your build file, not our API.

Do them in that order. Wiring the host first leaves you debugging two unfinished things at once.

## Part 1: your first unit

### Install the CLI

```bash
curl -fsSL https://interlock.sh/assets/install.sh | bash
```

One file, no package manager: the script (itself an Interlock unit) downloads the CLI from
`/assets/interlock-cli.js` and puts `interlock` on your PATH. Node 18+ is the only requirement —
the installer checks and says so if it is missing.

### Create the project

```bash
interlock init
```

This signs you in, writes a `.interlock-key` into the directory, and installs the Interlock skills
into your AI tools. The key binds this tree to one project on one server, and the CLI walks up from
the working directory to find it the same way git finds `.git`.

### Write a file

Ids are paths. A file at `code/hello.js` has the id `hello`.

```javascript
export default (req, il) => {
  const name = req.str('name', 'world');
  il.log('greeting ' + name);
  return { ok: true, msg: 'hello ' + name };
};
```

The default export is the handler. What you return becomes the response: an object is JSON, a string
is `text/plain`, and `html(...)` serves markup. A file with no handler is a **library unit**, which
other units can import but nobody can call as a page.

### Publish it and call it

```bash
interlock sync code
```

```bash
interlock run hello
```

```json
{ "ok": true, "msg": "hello world" }
```

Now change the message in the file, run `interlock sync code` again, and call it again. That is the
whole loop, and it is the thing worth feeling before you read anything else.

For a tighter loop, `interlock yolo code` mirrors the directory in both directions continuously.
Read [sync versus yolo](/operating#sync-versus-yolo) before you use it on a tree you care about.

### Java works the same way

A class instead of a function:

```java
package examples;

import sh.interlock.sdk.*;

public class HelloJava implements InterlockHandler<Greeting> {
    @Override
    public Greeting handle(Request req, Interlock il) {
        return new Greeting(true, "hello " + req.str("name", "world"));
    }
}
```

A Java unit's package is its directory, exactly as its id is its URL. So
`code/examples/HelloJava.java` declares `package examples;`. Getting this wrong is refused at
compile time with a sentence that says so. See [Java units](/java#directory-is-package).

Prefer a typed return over `Object` or `Map`. The interface between a unit and your JVM should be a
dumb POJO.

### Moving code towards production

Environments are `dev`, then `staging`, then `prod`, and code moves between them by **promotion**,
never by re-syncing:

```bash
interlock promote hello --from dev
```

A production host pins `prod`, so only a promotion changes what it runs. That is the entire safety
story for hot-deployable code, and it is why a careless sync cannot reach production on its own.

## Part 2: running units inside your own application

Everything above ran on an Interlock server. This part is what makes units useful: the same unit,
running in **your** process, able to call your classes and read your database.

### 1. Add the dependency

```groovy
dependencies {
    implementation 'sh.interlock:interlock-java-sdk:0.1.0-SNAPSHOT'
}
```

```xml
<dependency>
    <groupId>sh.interlock</groupId>
    <artifactId>interlock-java-sdk</artifactId>
    <version>0.1.0-SNAPSHOT</version>
</dependency>
```

While the version carries `-SNAPSHOT` it is **not on Maven Central**, and there is no public artifact
yet. Ask whoever operates your Interlock server which coordinate to use and where to resolve it
from: either an internal Maven repository they publish to, or a checkout of the Interlock repository
you build yourself. With a checkout that is one command, and then your build resolves it out of your
local repository:

```bash
./gradlew :interlock-java-sdk:publishToMavenLocal
```

```groovy
repositories { mavenLocal(); mavenCentral() }
```

### 2. The whole build file

This is the entire file rather than the Interlock fragment, because a fragment is not a build. Two
separate acceptance runs lost most of their time here, reinventing roughly 130 lines that nobody had
written down.

```groovy
plugins {
    id 'java'
    id 'io.quarkus' version '3.15.1'
}

repositories { mavenLocal(); mavenCentral() }   // mavenLocal FIRST: the SDK is a -SNAPSHOT

dependencies {
    implementation enforcedPlatform('io.quarkus.platform:quarkus-bom:3.15.1')
    implementation 'sh.interlock:interlock-java-sdk:0.1.0-SNAPSHOT'

    implementation 'io.quarkus:quarkus-arc'
    implementation 'io.quarkus:quarkus-rest'                    // the JAX-RS stack the /app passthrough assumes
    implementation 'io.quarkus:quarkus-rest-jackson'
    implementation 'io.quarkus:quarkus-hibernate-orm-panache'
    implementation 'io.quarkus:quarkus-jdbc-postgresql'         // or your driver
    implementation 'io.quarkus:quarkus-narayana-jta'            // units may open their own transaction
    implementation 'io.quarkus:quarkus-smallrye-health'         // /q/health, which /verify step 1 curls
}

java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }

// REQUIRED. Units are compiled at RUNTIME against the packaged application. If code/ leaks into a
// sourceSet, Gradle compiles them at BUILD time against the untransformed classpath instead, which
// silently defeats everything on the Quarkus page: entities have not been rewritten yet, so a unit
// that is wrong in production compiles clean here.
sourceSets { main { java { srcDirs = ['src/main/java'] } } }
```

`settings.gradle` needs the plugin repository or `id 'io.quarkus'` will not resolve:

```groovy
pluginManagement {
    repositories { gradlePluginPortal(); mavenCentral() }
}
rootProject.name = 'my-host'
```

**Where `code/` lives.** At the repository root, a sibling of `build.gradle`, and never under
`src/`. The gate script assumes it (`UNITS=code`), the CLI assumes it, and the `sourceSets` line
above is what keeps Gradle out of it.

**Which JAX-RS stack.** `quarkus-rest`, which is RESTEasy Reactive. The `/app` passthrough on
[Embedding the SDK](/host#the-app-passthrough-in-full) is written against it, and a `String body`
method parameter behaves differently on the classic stack.

### 3. Build the Engine

`InterlockSDK.init` takes your SDK key and returns a builder. It is fluent, and `build()` returns the
`Engine`:

```java
Engine engine = InterlockSDK.init(System.getenv("INTERLOCK_SDK_KEY"))
        .codeSource(new InterlockClient(url, key))
        .stores(myStores)
        .secrets(mySecrets)
        .context("app", myContext)            // your own class, whatever it is
        .build();
```

There are three `init` overloads and none of them takes a configuring lambda: `init(String sdkKey)`,
`init(String sdkKey, Map<String, Object> context)`, and `init(String sdkKey, ContextProvider context)`.

**Use `build()` rather than constructing `new Engine(...)` yourself.** `build()` is what auto-anchors
your context classes for javac. Skip it and units fail to compile with
`package com.example.billing does not exist`.

An anchor is how javac finds your host's classes: the anchor's protection-domain code source **is**
the host classpath entry, and registering one also reserves its package against units declaring into
it. **You do not normally call `Engine.addCompileAnchor` yourself**, because `build()` anchors every
class you seeded as context. Call it by hand only when you use a dynamic `ContextProvider`, or when
units name a type you never seed. Full wiring on [Embedding the SDK](/host#compile-anchors).

Packages, because getting these wrong costs you a compile: `sh.interlock.sdk` holds `InterlockSDK`,
`Request`, `Interlock`, `Refusal`, `Html` and `Json`. **`Engine` and `Result` are in
`sh.interlock.sdk.runtime`**, along with `CodeSource`, `DirectoryCodeSource`, `StoreFactory`,
`SecretResolver` and the exception types. The full table is on
[Reference](/reference#packages-at-a-glance).

### 4. Which secret is which

Five names, and mixing them up fails quietly, so keep them straight:

| Name | What it is | Who reads it |
|---|---|---|
| `INTERLOCK_SDK_KEY` | your HOST's key, minted per project | `InterlockSDK.init(...)` in your application |
| `.interlock-key` | the same kind of key, written into a project directory by `interlock init` | the CLI, so it knows which project a tree belongs to |
| `INTERLOCK_URL` | which Interlock server to talk to | both the SDK and the CLI |
| `INTERLOCK_ENV` | which env the SDK fetches units from | the SDK |
| `interlock.env` | the same choice, as Quarkus config | your host, if you prefer properties to environment |

The host key and the CLI key are the same kind of secret and can be the same value. Set
`INTERLOCK_ENV` (or `interlock.env`) deliberately: pointing a production host at `dev` is silent and
serves the wrong code.

## What will bite you

Three things, in the order people meet them.

**The project key binds the tree.** `.interlock-key` ties that directory to one project on one
server. Run the CLI in a tree you copied and you publish into somebody else's project. Read
[Operating a project](/operating) before your first sync.

**Quarkus rewrites your entities after your build.** If your host is Quarkus and your units touch
Panache entities, read [Quarkus](/quarkus) before you write the first one. Quarkus builds every
entity twice and the JVM loads the rewritten copy. A direct field read compiles clean and then dies
on the first real request:

```
java.lang.IllegalAccessError: tried to access protected field com.example.billing.Note.uuid
```

The rule: **give unit-facing entities private fields with hand-written accessors, and read them
through those accessors, including the identity field.** A field literally named `id` keeps its
`public` modifier and so appears to work, which is exactly why this hides. Name it `uuid`, `ref` or
`hash` and it is privatized like anything else. Full table, the `javap` commands that measured it,
and the mechanism: [the entity rule](/quarkus#the-entity-rule).

**A 200 proves nothing.** It proves your request was accepted, not that it did anything. Assert
effects, not status codes, and read the body back. That distinction is the only reason a bug where
every POST body was silently ignored was ever found.

## Before you call it working

Run the [verification sequence](/verify). It is eight commands and it checks the things that fail
quietly rather than the things that fail loudly.

If you are an agent rather than a person: read `/llms-full.txt` instead of clicking through these
pages. It is this page and every other one, in a single fetch, in dependency order.
