# Sync to Northflank

Northflank injects a secret group's values into the services and jobs that
inherit it, at deploy time. A binding writes one secret group — the platform's
unit of injection.

| At a glance | |
| --- | --- |
| **What seekrit writes** | A secret group's `variables` map |
| **Addressed by** | Project slug + secret group slug |
| **Connection carries** | The token alone — a Northflank token names its own team |
| **Token permission** | An API role granting **Secrets → Secret Groups**: read and update on the project |
| **Takes effect** | Next deploy, or **restart dependents** on the group's page |
| **Value visibility** | Readable by anyone with access to the project's secret groups |

New to sync? Read [Third-party sync](/docs/guides/third-party-sync) first — the
decryption grant, name mapping, deletions, and failure handling are the same on
every destination.

## 1. Create an API token

In Northflank, go to **Team settings → API tokens** and create one with a role
granting **Secrets → Secret Groups** read and update on the project you are
syncing into. Read is not optional here — see
[why this connector reads first](#why-a-push-reads-the-group-first).

A Northflank token is issued by exactly one team and carries that scope itself,
so a Northflank connection has nothing else to configure:

```bash
printf '%s' "$NORTHFLANK_API_TOKEN" | seekrit sync connect \
  --name acme-northflank --provider northflank
```

## 2. Bind an environment

A binding writes one **secret group**, which the project's services and jobs
inherit. Create the group in Northflank first (**Project → Secrets → Create
secret group**), then name it and its project by the slug in their URLs, not by
their display names:

```bash
seekrit sync verify acme-northflank --provider northflank \
  --project default-project --secret-group app-secrets

seekrit sync enable --connection acme-northflank --provider northflank \
  --project default-project --secret-group app-secrets \
  --app storefront --env production --acknowledge-decryption
```

seekrit checks both against Northflank's own slug pattern before storing them,
so the common slip — pasting the display name, spaces and all — fails at the form
rather than as a bare 404 inside a run nobody is watching.

Verify walks the project's secret-group **metadata** (names and ids, never
values) to confirm the group exists and the token can see it.

There is no environment field. Northflank has no per-group environment axis —
separate environments are separate projects, or separate groups restricted to a
stage — so a seekrit environment maps to a group, one to one.

seekrit writes only the group's **variables**. Its priority, restrictions, secret
type, secret files, and linked addons are yours: a push leaves them exactly as
you configured them.

> **Note:** Northflank injects a secret group's values at **deploy time**. A synced change reaches a running service on its next deploy, or when you use **restart dependents** on the group's page. seekrit does not restart anything on your behalf.

## Why a push reads the group first

Northflank's variables map is **replace-semantics**: `PATCH` with
`secrets.variables` sets the group's map to exactly what was sent, so a key
absent from the payload is deleted. There is no per-variable endpoint and no
merge flag — the whole map is the unit of writing.

So a run reads the group's current variables and writes back
`(existing − removed) ∪ seekrit`. Sending only seekrit's names would be one
request instead of two, and would silently delete every variable in the group
that seekrit does not manage — a hand-added `LOG_LEVEL`, another team's key. A
secrets manager may fail a push; it may not quietly destroy the values beside it.

> **Warning:** **That read is the one place a connector sees plaintext seekrit did not already hold**, so it is worth being precise about what it costs. The read uses `show=this`, which excludes secrets inherited from linked addons — database credentials and the like — so only the group's own variables are read. In the recommended setup, a group dedicated to this binding, every value read is one seekrit just decrypted anyway. The read only sees *foreign* values in exactly the case where not reading would destroy them, and they are used for one thing: writing them straight back, unchanged. Nothing is cached, logged, audited, or kept past the run. Give a binding a secret group of its own and this never arises — everything in it came from seekrit to begin with.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| `401` or `403`, whole run stopped | The token was revoked, or its role lacks Secret Groups read **or** update on this project | Both permissions are required — the read is not optional, see above |
| `404` on the group, whole run stopped | The group doesn't exist, or the slug is wrong | Create it in Northflank first; seekrit will not. Take both slugs from the resource URL |
| A slug is rejected when enabling | A display name was pasted instead of the URL slug | Slugs are lowercase, hyphen-separated, with no spaces |
| A hand-added variable disappeared | Something wrote the group's map without merging | seekrit always merges. Check for another tool, or a `PATCH` from your own automation |
| Values pushed, service unchanged | Northflank injects at deploy time | Deploy, or use **restart dependents** on the group |
| Run reported partial, `429` in the error | Northflank rate-limited the token | Nothing to do — seekrit honors `Retry-After` and retries |

## See also

- [Third-party sync](/docs/guides/third-party-sync) — the shared model, naming and filtering, deletions
- [CLI reference](/docs/reference/cli#third-party-sync) — every `seekrit sync` flag
