A local development edge router and name registry.
Devedge gives every project stable HTTPS hostnames on one shared 80/443 entry point, and lets host apps, containers, and k3d clusters register routes dynamically.
Full documentation: https://infobloxopen.github.io/devedge/. This README is a quick tour; the
site has the getting-started guide, the de CLI reference, the concepts, and the end-to-end
full-stack walkthrough.
devedge is the dev- and deploy-time edge of a small ecosystem. Each piece is usable on its own and
composes with the others through the de CLI:
| Project | Role | Docs |
|---|---|---|
| devedge (this repo) | The local dev edge router and name registry, and the de CLI that scaffolds, builds, routes, and deploys services and micro-frontends. |
https://infobloxopen.github.io/devedge/ |
| devedge-sdk | The runtime library a Go service imports: a gRPC + REST service from one proto, with fail-closed authorization, multi-tenant isolation, and persistence and eventing behind swappable seams. | https://infobloxopen.github.io/devedge-sdk/ |
| devedge-ufe-sdk | The frontend counterpart: an Angular / single-spa micro-frontend SDK where the shell owns the session. | https://infobloxopen.github.io/devedge-ufe-sdk/ |
| apx | The API schema lifecycle tool de drives to publish a service's OpenAPI v3 to a catalog and generate typed clients. |
https://apx.infoblox.dev/ |
de new service builds on devedge-sdk, de ufe new builds on devedge-ufe-sdk, and de api publish
drives apx — see Full-stack: a service and a micro-frontend
for the path end to end.
devedge ships a Claude Code skills marketplace —
infobloxopen/devedge-claude-plugins —
so you can drive the common operations from a prompt instead of memorizing commands. Add the
marketplace once, then install the skills you want:
/plugin marketplace add infobloxopen/devedge-claude-plugins
/plugin install new-service@devedge # or publish-api, run-locally, new-ufe, compose-services, model-domain
Each skill triggers on intent and drives its operation end to end using the real de / apx
tooling and the published docs as ground truth:
| Skill | Use it to |
|---|---|
new-service |
Bootstrap and build a microservice on devedge-sdk from a single prompt |
model-domain |
Model the domain with DDD rigor — aggregate boundaries, references, read surfaces |
publish-api |
Publish the service's OpenAPI v3 through apx and generate a typed client |
run-locally |
Bring the service up on the local edge and round-trip it over its *.dev.test host |
new-ufe |
Scaffold an Angular / single-spa micro-frontend and register it into a shell |
compose-services |
Compose several service modules into one suite binary and deploy it |
For the walkthrough, see Use with Claude Code.
brew tap infobloxopen/tap
brew install --cask infobloxopen/tap/devedgeOr build from source:
make build # binaries in ./bin/de install # install daemon, mkcert CA, DNS config
de start # start the background daemon
de doctor # verify everything is healthyIf your app runs directly on the host (e.g. npm run dev, go run .):
- Add a
devedge.yamlto your project root:
apiVersion: devedge.infoblox.dev/v1alpha1
kind: Config
metadata:
name: myapp
spec:
defaults:
ttl: 30s
tls: true
routes:
- host: myapp.dev.test
upstream: http://127.0.0.1:3000
- host: api.myapp.dev.test
upstream: http://127.0.0.1:4000- Start your app, then register routes:
npm run dev & # or whatever starts your app
de project up # registers all routes from devedge.yaml-
Open
https://myapp.dev.testin a browser — trusted HTTPS, no port numbers. -
When done:
de project downFor long-running sessions, use de project up --watch to keep leases alive with
automatic heartbeats.
For quick one-off routes without a config file:
de register myapp.dev.test http://127.0.0.1:3000
de register api.myapp.dev.test http://127.0.0.1:4000 --project myapp
# When done
de unregister myapp.dev.test
# Or remove all routes for a project
de project down myappIf your project runs in a k3d cluster:
# Create a cluster with devedge integration pre-configured
de cluster create myapp
# Deploy your app normally
kubectl apply -f k8s/
# Option A: explicit route attachment
de cluster attach myapp \
--host api.myapp.dev.test \
--host web.myapp.dev.test
# Option B: auto-register from Ingress objects
# Annotate your Ingress with devedge.io/expose=true, then:
de cluster watch myappFor full Kubernetes-native integration (cert-manager + external-dns), bootstrap the cluster first:
de cluster bootstrap myappThis installs the mkcert CA into the cluster so cert-manager can issue locally-trusted certificates, and deploys an external-dns webhook that automatically registers Ingress hostnames with devedge.
Your app's Ingress manifests work unchanged between local dev and production — only the cluster-level issuer and DNS provider differ.
Devedge can proxy TCP services like databases with SNI-based TLS:
apiVersion: devedge.infoblox.dev/v1alpha1
kind: Config
metadata:
name: myapp
spec:
routes:
- host: api.myapp.dev.test
upstream: http://127.0.0.1:3000
- host: postgres.myapp.dev.test
upstream: 127.0.0.1:5432
protocol: tcp
- host: redis.myapp.dev.test
upstream: 127.0.0.1:6379
protocol: tcpOr via CLI:
de register postgres.myapp.dev.test 127.0.0.1:5432 --protocol tcpConnect with TLS-aware clients:
psql "host=postgres.myapp.dev.test sslmode=require"A typical full-stack project config:
apiVersion: devedge.infoblox.dev/v1alpha1
kind: Config
metadata:
name: datakit
labels:
team: platform
spec:
defaults:
ttl: 30s
tls: true
routes:
- host: web.datakit.dev.test
upstream: http://127.0.0.1:3000
- host: api.datakit.dev.test
upstream: http://127.0.0.1:8080
- host: grpc.datakit.dev.test
upstream: 127.0.0.1:50051
protocol: tcp
- host: postgres.datakit.dev.test
upstream: 127.0.0.1:5432
protocol: tcp
- host: redis.datakit.dev.test
upstream: 127.0.0.1:6379
protocol: tcp# Start all services, then:
de project up --watch
# Everything reachable via stable hostnames:
# https://web.datakit.dev.test
# https://api.datakit.dev.test
# psql "host=postgres.datakit.dev.test sslmode=require"
# redis-cli -h redis.datakit.dev.test --tlsdevedge manages which Kubernetes cluster a project lands on. You never need to
create a cluster or switch your kubectl context manually.
de project up selects the target cluster from an explicit topology model:
| Condition | Target cluster | Mode printed |
|---|---|---|
| Default (developer machine) | devedge |
shared dev |
CI=true (or any truthy value) |
devedge-ci-<runid> |
ephemeral |
spec.cluster.dedicated: true in config |
devedge-proj-<slug> |
dedicated |
The --env flag (or DEVEDGE_ENV env var) overrides auto-detection:
--env dev, --env ci, or --env ephemeral. The override always takes
precedence over the CI variable.
On a developer machine, all projects share one cluster named devedge. The
first de project up for a project that declares dependencies creates and
bootstraps it (installs cert-manager, the devedge ClusterIssuer, and the
external-dns webhook). Subsequent calls for any project reuse the same cluster.
de project up
# cluster: devedge (shared dev)
# dependency db (postgres) ready
# DATABASE_URL=fsnotify://...- Your
kubectlcontext is never changed. - Concurrent first-time
de project upcalls are serialized by a host-level lock (~/.devedge/cluster-devedge.lock); exactly one cluster is created. - A project with no dependencies still resolves and reports the cluster but does not trigger a cluster create.
de ci run -- <command...> wraps a command in a full ephemeral-cluster
lifecycle. It creates a dedicated devedge-ci-<runid> cluster, runs the
command with the cluster's context available as DEVEDGE_KUBECONTEXT, and
tears the cluster down on every exit path — success, failure, or interrupt.
The wrapped command's exit code is propagated.
# In CI (e.g. GitHub Actions):
de ci run -- go test ./test/e2e/...
# cluster: devedge-ci-<runid> (ephemeral)
# <test output>
# cluster torn down on exitConcurrent runs each receive a distinctly named cluster (devedge-ci-<runid>
where <runid> comes from GITHUB_RUN_ID, DEVEDGE_RUN_ID, or a random
token) and never interfere with each other. The CI workflow never calls k3d
directly.
A project that cannot safely coexist with others (e.g. it must mutate
cluster-global state) can declare spec.cluster.dedicated: true in its
devedge.yaml:
apiVersion: devedge.infoblox.dev/v1alpha1
kind: Service
metadata:
name: heavy-svc
spec:
dev:
hostname: heavy-svc.dev.test
cluster:
dedicated: true # own cluster instead of the shared dev cluster
dependencies:
- name: db
engine: postgres
port: 5432de project up
# cluster: devedge-proj-heavy-svc (dedicated)
de project down --clean # also removes the dedicated clusterProjects without the opt-in continue to land on the shared devedge cluster.
Within a shared cluster, a dependency can request its own engine instance instead of attaching to the shared per-engine one:
dependencies:
- name: db
engine: postgres
port: 5432
dedicated: true # own Postgres instance; not the shared per-engine oneUse only when per-service logical isolation inside the shared instance is not
enough. For full isolation, prefer cluster.dedicated: true.
de install Install daemon and configure the system
de start Start the daemon
de stop Stop the daemon
de doctor Check system health
de status Show daemon status
de ui Open the web dashboard
de register HOST UPSTREAM [--project P] [--ttl 30s] [--protocol tcp] [--backend-tls]
de unregister HOST
de renew HOST
de ls [--json]
de inspect HOST
de project up [-f devedge.yaml] [--watch] [--env dev|ci|ephemeral] [--deploy]
de project down [PROJECT] [-f devedge.yaml] [--clean]
de project chart [-f devedge.yaml] [-o DIR]
de ci run -- COMMAND [ARGS...]
de cluster create CLUSTER [--port 8081]
de cluster delete CLUSTER
de cluster bootstrap CLUSTER [--force]
de cluster attach CLUSTER --host api.foo.dev.test [--ingress URL]
de cluster detach CLUSTER
de cluster ls
de cluster watch CLUSTER
de k3d ... (alias for de cluster)
de new service NAME [-- SDK_FLAGS...] Scaffold an apx-native Go service and route it
de compose init|add|remove|tidy|build|test|up|chart Compose service modules into one suite binary
de cell create|down|status|assign|move|rebalance Manage cell-based deployments and tenant routing
de api publish Publish a service's OpenAPI v3 spec to the apx catalog
de ufe new NAME Scaffold an Angular + single-spa micro-frontend
The project configuration follows the Kubernetes resource API structure:
apiVersion: devedge.infoblox.dev/v1alpha1
kind: Config
metadata:
name: foo
labels:
team: platform
spec:
defaults:
ttl: 30s
tls: true
routes:
- host: web.foo.dev.test
upstream: http://127.0.0.1:3000
- host: api.foo.dev.test
upstream: http://127.0.0.1:8081
- host: db.foo.dev.test
upstream: 127.0.0.1:5432
protocol: tcpde project init NAME [--dir DIR] [--module MODULE] generates a complete, ready-to-run service
project in one step. NAME must be a lowercase DNS label (e.g. webhooks); it becomes the
stable dev hostname, the Helm release name, and the default Go module base. The command refuses to
write into a non-empty target directory.
de project init webhooks # creates ./webhooks/
de project init webhooks --dir ~/src/webhooks --module github.com/acme/webhooksWhat gets generated:
devedge.yaml(kind: Service) — Postgres dependency withmigrationsdeclared and aspec.workload.buildblock sode project up --deployworks out of the box.- Proto definition — one example resource (
WebhookEndpoint) with a full CRUD RPC set. Every RPC carries aninfoblox.authz.v1.ruleannotation so the authorization contract is explicit in the proto, not scattered through implementation. - Generated code — gRPC server stubs and a REST/JSON gateway (run
make generateafter init; the Makefile preflights the required tools —buf,protoc-gen-go,protoc-gen-go-grpc,protoc-gen-grpc-gateway— and names anything missing). - Fail-closed server — the server checks at boot that every RPC in the service descriptor has a declared authz rule; an undeclared method causes the process to refuse to start rather than silently serve open.
- Initial migration —
0001_webhook_endpoints.up.sql/.down.sql(4-digit sequential numbering) to bootstrap the schema. - Dockerfile — multi-stage build that produces a
migratesubcommand alongside the server binary, satisfying the deploy hook contract (de project up --deployruns<image> migrate upbefore the Deployment rolls). AGENTS.mdandREADME.md— a rename checklist and a getting-started guide for the scaffolded project.
Generated projects depend only on released public modules (github.com/infobloxopen/devedge-sdk
and the canonical authz annotation module github.com/infoblox/authz-annotations); no internal
forks or replace directives are required.
The loop after init:
cd webhooks
make generate # regenerate proto; needs buf + protoc-gen-* on PATH
de project up # provision Postgres, apply migration, register routes
make run # start the server locally
# CRUD over stable HTTPS:
curl https://webhooks.dev.test/v1/webhook-endpoints
curl -X POST https://webhooks.dev.test/v1/webhook-endpoints \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/hook"}'For in-cluster: de project up --deploy (builds the image, loads it into the dev cluster, runs
the migration Job, then rolls the Deployment).
de new service NAME [--resource RESOURCE] [--backend BACKEND] [--dir DIR] [-- DEVEDGE_SDK_FLAGS...]
is a thin driver over the devedge-sdk scaffold. Use
it when you want an apx-native, authz-gated, persisting gRPC + HTTP service from day one:
- Forwards to
devedge-sdk new servicefor the heavy lifting (annotated proto, generated models + repository + gRPC server + REST/JSON gateway, fail-closed authz boot gate). - Emits a
devedge.yaml(kind: Config) routing the service's HTTP/JSON gateway through the local edge sode project upserves it over stable HTTPS immediately.
de new service orders --resource Order
de new service notes --resource Note --backend ent
de new service orders --resource Order --dir ./services/orders -- --module github.com/acme/ordersWhen to use which scaffold:
| Command | Use when |
|---|---|
de new service |
You want an apx-governed proto, authz rules, and generated server from the start; devedge-sdk manages the code generation lifecycle |
de project init |
You want a conventional in-tree service scaffold (Postgres migration, Dockerfile, proto stubs) without adopting apx codegen |
Requires devedge-sdk on PATH:
go install github.com/infobloxopen/devedge-sdk/cmd/devedge-sdk@latestIn addition to kind: Config, devedge understands kind: Service — a service-oriented
project file that routes exactly like Config but also declares its development hostname and
runtime dependencies. Unlike Config, a Service document is parsed strictly: unknown
fields are rejected to catch typos.
apiVersion: devedge.infoblox.dev/v1alpha1
kind: Service
metadata:
name: webhooks
spec:
dev:
hostname: webhooks.dev.test # required; valid hostname
cluster: # optional; cluster placement (feature 004)
dedicated: false # true → own cluster (devedge-proj-<slug>)
workload: # optional; enables `de project up --deploy` (feature 005)
image: ghcr.io/acme/webhooks:dev # EITHER a pre-built image reference
# build: # OR build from the project (exactly one of image/build)
# context: .
# dockerfile: Dockerfile # optional, defaults to Dockerfile
port: 8080 # required when workload is set
replicas: 1 # optional, default 1
dependencies: # optional; started on `de project up`
- name: db
engine: postgres # postgres | redis
version: "16" # optional
port: 5432 # 1-65535
dedicated: false # true → own per-service engine instance (rare)
- name: cache
engine: redis
port: 6379
routes: # optional; same shape as Config routes
- host: webhooks.dev.test
upstream: http://127.0.0.1:8080de project up registers the routes and starts the declared dependencies. The full schema and
error contract are documented in
specs/002-service-config-kind/contracts/service-config.md.
When a Service declares dependencies, de project up makes them real and reachable (requires the
helm, kubectl, and k3d CLIs):
- Shared instance per engine, isolated per service. devedge runs one Postgres and one Redis in the dev cluster (installed via Helm) and gives each service its own database + credentials (Postgres) or ACL user + key namespace (Redis), so co-located services never see each other's data.
- Connection by hotload DSN. For each dependency devedge writes the real DSN to
~/.devedge/services/<service>/<dep>.dsn(mode0600) and reports an indirect env var the app consumes — e.g.DATABASE_URL=fsnotify://postgres/<path-to-file>(theinfobloxopen/hotloadpattern; the app reads the real DSN from the file and hot-reloads on change). The same shape is emitted for every engine (REDIS_URL=fsnotify://redis/<path>).
de project up # starts deps, prints each env var + DSN file, then registers routes
de project down # releases deps; KEEPS data by default
de project down --clean # also drops this service's database/keys
de project chart -o ./chart # emit a Helm chart for the service + abstract dependency claimsData persists across down/up; --clean drops only the requesting service's data, never the
shared instance. de project chart emits (does not deploy) a Helm chart expressing dependencies as
abstract claims, so the same declaration maps to a shared logical database in dev and a dedicated
instance in a real cluster. See
specs/003-dependency-runtime/ for the design and contract.
By default de project up runs the service locally. To run the service inside the resolved
cluster — next to its dependencies — add a spec.workload block to your kind: Service config
and pass --deploy:
de project up --deploy
# cluster: devedge (shared dev)
# dependency db (postgres) ready
# deployed: my-svc -> cluster devedge (1 replica(s)) https://my-svc.dev.testDeclare a pre-built image:
apiVersion: devedge.infoblox.dev/v1alpha1
kind: Service
metadata:
name: my-svc
spec:
dev:
hostname: my-svc.dev.test
workload:
image: ghcr.io/acme/my-svc:dev # pre-built reference
port: 8080
replicas: 1 # optional, default 1
dependencies:
- name: db
engine: postgres
port: 5432Or build from the project (no external registry needed — the image is loaded straight into the
cluster via docker build + k3d image import):
workload:
build:
context: .
dockerfile: Dockerfile # optional, defaults to Dockerfile
port: 8080Exactly one of image or build must be set; port is required.
In-cluster dependency connection. A deployed workload reaches its dependencies over the
in-cluster Service DNS (e.g.
devedge-postgres.devedge-deps.svc.cluster.local:5432) using per-service credentials delivered
via an in-cluster Secret (<service>-<dep>-dsn) that devedge creates at deploy time. The same
003 binding identity (database, user, password) is reused; only the reachable host differs.
Routing. The service chart includes an Ingress annotated devedge.io/expose=true for
spec.dev.hostname, so the deployed workload is reachable over its stable dev hostname via
devedge's existing ingress-watch path.
Idempotent. Re-running de project up --deploy after a change rolls out the running workload
with no duplicate release.
Teardown. de project down removes the deployed workload (helm uninstall, footprint-only —
never the shared cluster or another project's workload). It is a no-op for services that were
never deployed. --clean dependency-data semantics (003) and dedicated-cluster removal (004) are
unchanged.
Coexistence. Multiple services deployed to the shared dev cluster each get a distinct release (named by service slug) and a distinct Ingress host; taking one down leaves the others running.
See specs/005-app-workload-deploy/ for the full design.
A postgres dependency can declare versioned schema migrations and optional dev seed data. Add
migrations and/or seed to the dependency block in your kind: Service config:
dependencies:
- name: db
engine: postgres
migrations: db/migrations # dir of NNN_name.up.sql / NNN_name.down.sql (golang-migrate style)
seed: db/seed/dev.sql # optional SQL file or dir; dev-onlyBoth fields are optional and accepted only on engine: postgres — declaring them on any other
engine is a parse/validate error. Paths resolve under the project root and must exist; the
migrations directory must contain at least one *.up.sql. seed without migrations is allowed.
de project up: when a dependency declares migrations, devedge brings its isolated database to
the declared schema version before the dependency is marked ready and before the workload
serves — in both local-run and --deploy modes. The migrate step targets a version (the highest
migration in the source or image) and migrates up or down to reach it, so deploying an older
image automatically rolls the schema back with no separate rollback command. Applied down steps are
persisted to a store (a host directory in local-run mode; a per-service PVC in deploy mode) so a
rollback works even when the current image no longer ships those .down.sql files. The step is
fully idempotent and reports the outcome:
✔ db: migrations applied 2 (v0 → v2)
✔ db: seed seeded
Re-running with no new migrations reports already current / already seeded and makes no
changes. In CI (de ci run), seed is skipped; schema migrations still run.
Failure recovery: if a migration fails, up stops with an actionable error and the workload
does not serve against a partial schema. A corrected re-run auto-recovers the dirty state. For a
botched migration where the SQL itself was wrong, the reliable fix is de project down --clean
followed by de project up (rebuilds the corrected schema from scratch).
de project down / --clean: plain down preserves the schema and data. --clean removes
the schema, seed marker, and the persisted down-migration store in addition to the dependency data,
so the next up rebuilds everything from scratch.
Deploy-mode migrate subcommand contract: when --deploy is used with declared migrations,
devedge renders a Helm pre-install/pre-upgrade hook Job that runs the service's own image
as <image> migrate up before the Deployment rolls. Images must provide a migrate subcommand
that reads DATABASE_URL (from the per-dep DSN Secret) and DEVEDGE_DOWNSTORE (the persisted
down-store path), converges the bundled migrations to their target version, and exits non-zero on
failure. If the image does not provide this subcommand, de project up --deploy fails with an
actionable error.
See specs/006-storage-migrations-seed/ for the full design
and specs/006-storage-migrations-seed/contracts/migrations-contract.md
for the service-image subcommand contract.
devedge scaffolds and routes both halves of a feature — a Go backend service and an Angular micro-frontend — so one developer can build the whole path:
de new service orders # scaffold a Go service (built on devedge-sdk)
de project up # provision deps, migrate, route at https://orders.dev.test
de api publish # publish the service's OpenAPI v3 to the apx catalog
de ufe new orders-ufe # scaffold an Angular micro-frontend (built on devedge-ufe-sdk)- The Go service is built on
devedge-sdk; its documentation lives at https://infobloxopen.github.io/devedge-sdk/. - The micro-frontend is built on
devedge-ufe-sdk. The shell owns the session, and a bearer interceptor attaches the token to the requests the generated API client makes. - For a complete worked example — the backend and the micro-frontend wired end to
end — see
examples/fullstack-oss.
Compose multiple services into one suite binary with de compose, and deploy
across tenant cells with de cell.
make test # run all tests
make lint # go vet
make build # build de + devedged + devedge-dns-webhookcmd/de CLI for developers and project automation
cmd/devedged background daemon (control plane)
cmd/devedge-dns-webhook external-dns webhook provider for k8s integration
internal/registry lease-based route registry with conflict detection
internal/reconciler event-driven sync: Traefik configs + /etc/hosts + certs
internal/render Traefik dynamic + static config generation
internal/daemon HTTP API over Unix socket + TCP + web dashboard
internal/client Go client for the daemon API
internal/dns /etc/hosts management + macOS /etc/resolver/ drop-in
internal/certs mkcert integration for locally-trusted TLS
internal/platform OS adapters: macOS LaunchAgent, Linux systemd
internal/cluster provider-based cluster management (k3d, extensible)
internal/k3d k3d-specific discovery and ingress watcher
internal/traefik Traefik subprocess lifecycle management
internal/externaldns external-dns webhook protocol implementation
pkg/types shared domain types (Route)
pkg/config project config parser (k8s resource API structure)
See product_vision.md for the full design.