A complete, secure-by-default microservice from a single proto. Define a resource and its methods in proto; devedge-sdk gives you a running gRPC + REST service with Google-AIP semantics, fail-closed authorization, multi-tenant isolation, and a persistence/eventing layer wired in β every backend a swappable seam, every security invariant provable in CI.
It is the runtime companion to devedge: devedge is the
dev- and deploy-time edge; devedge-sdk is the runtime library that production services import.
Status: early. APIs will change. Pin a version and read the changelog/releases before upgrading. The operational baseline is still filling in β built-in observability and health/readiness probes are tracked on the foundation roadmap (#97).
Full docs live at infobloxopen.github.io/devedge-sdk. Start here:
| Section | What's there |
|---|---|
| π Getting Started | Install the SDK and stand up a running, fail-closed service in five minutes (Quickstart). |
| π‘ Concepts | The architecture and seam model, the annotation contract, tenant isolation, aggregates, and events. |
| π Guides | Task how-tos: define a service, model a resource, pick a storage shape, run seccheck. |
| π Reference | Per-package API reference and codegen-plugin docs. |
| π Tutorial | Build the API Key Manager service end to end. |
- From one proto to a running, AIP-correct service. Scaffold a service, declare your resource and
methods, and
server.Newstands up gRPC plus an optional HTTP/JSON gateway with the standard methods and Google-AIP semantics already wired: field-maskPATCH(AIP-134), ETag/If-Matchβ412 optimistic concurrency (AIP-154), pagination, filtering (AIP-160), soft-delete + undelete (AIP-148/149), batch methods (AIP-137), request de-duplication (AIP-155), and long-running operations (AIP-151/152). Correct API semantics, for free. - Secure by default β and provable. Authorization is fail-closed: annotate each RPC with
(infoblox.authz.v1.rule)and the service refuses to boot if any served method is undeclared. Every query is scoped byaccount-idat the storage layer (GORM and ent), so one principal can never see another's resources; secret-annotated fields are encrypted at rest and never returned; and errors never leak SQL or stack traces. Theseccheckpackage proves all of it in CI. - Pluggable seams, dev defaults, zero service-code change. Persistence (in-memory β GORM/ent), transactions, domain events (in-memory β Kafka, via a transactional outbox), the authz decision point, and the secret encryptor each ship a dev-suitable default and swap for a production backend without touching service code. DDD aggregates and multi-surface projections are first-class.
- Codegen from your proto; dependency-light core.
protoc-gen-svcscaffolds the service,protoc-gen-storageemits a GORM repository,protoc-gen-entemits an ent schema, andprotoc-gen-devedge-authzemits the authz-rules table β the proto is the single source of truth. Core packages depend only on the standard library: no ORM, no policy-engine dependency.
go get github.com/infobloxopen/devedge-sdk@latestInstall the codegen plugins onto your PATH so buf generate can invoke them:
go install github.com/infobloxopen/devedge-sdk/cmd/protoc-gen-svc@latest
go install github.com/infobloxopen/devedge-sdk/cmd/protoc-gen-storage@latest
go install github.com/infobloxopen/devedge-sdk/cmd/protoc-gen-ent@latest
go install github.com/infobloxopen/devedge-sdk/cmd/protoc-gen-devedge-authz@latestRequires Go 1.25+ and buf. Postgres and Vault are only needed for the production storage and secret backends β the in-memory store and dev encryptor run in-process. Full prerequisites: Installation.
In about five minutes you go from a .proto to a running, fail-closed gRPC + REST service.
1. Declare each RPC's authz requirement in proto β verb + resource is all a method needs:
service WidgetService {
rpc GetWidget(GetWidgetRequest) returns (Widget) {
option (infoblox.authz.v1.rule) = {verb: "get", resource: "widget:{id}"};
}
rpc CreateWidget(CreateWidgetRequest) returns (Widget) {
option (infoblox.authz.v1.rule) = {verb: "create", resource: "widget"};
}
}2. Generate (buf generate) β protoc-gen-devedge-authz emits a WidgetServiceAuthzRules
table next to the .pb.go.
3. Wire the server. The Authorizer defaults to default-deny, so every call is denied
until you grant something β fail-closed by construction:
srv, err := server.New(server.Config{
GRPCAddr: ":9090",
HTTPAddr: ":8080", // optional HTTP/JSON gateway; omit to run gRPC-only
Rules: widgetv1.WidgetServiceAuthzRules, // generated in step 2
// Dev decision point β grant group:admin everything. Swap for an
// OPA/Cedar/remote Authorizer in production; nothing else changes.
Authorizer: authz.NewDevAuthorizer(authz.Grant{
Tenant: "t1", Subjects: []string{"group:admin"},
Verbs: []authz.Verb{"*"}, Resource: "*",
}),
// Derive the principal from request metadata (account-id β tenant, groups β
// group:<name>). Use a verified-token func in production.
PrincipalFunc: grpcauthz.DevPrincipalFunc(),
})
if err != nil {
log.Fatal(err)
}
widgetv1.RegisterWidgetServiceServer(srv.GRPCServer(), &widgetServer{})
log.Fatal(srv.Serve(ctx)) // blocks until ctx is cancelledYou now have a running, fail-closed service speaking gRPC and REST. The chain server.New builds,
outermost first:
RequestID β ErrorMapper β TenantID β grpcauthz (fail-closed) β FieldMask β ETag/412 β ReadMask β ValidateOnly β Deduplicate
β Full walkthrough with tests: Quickstart.
| Package | What it provides |
|---|---|
server |
Server lifecycle: gRPC + optional HTTP/JSON gateway, the interceptor chain auto-wired, graceful shutdown. |
middleware |
The interceptors: RequestID, TenantID, FieldMask, ErrorMapper, ValidateOnly, Dedup, and etag. |
authz |
Engine-neutral model: Principal, Resource, Verb, AccessRequest, Decision, the pluggable Authorizer, and DevAuthorizer (in-process, default-deny). |
authz/grpcauthz |
Fail-closed gRPC interceptor + boot gate. Rough-compatible with atlas-authz-middleware/grpc_opa (COMPAT.md). |
authz/catalog |
Builds the permission catalog (per resource: verbs, endpoints, View/Manage groups) from declared rules. |
authz/authzpb |
Reflection-based rule extractor β reads (infoblox.authz.v1.rule) off linked descriptors, no generated file. |
persistence |
ORM-free Repository[T,K] seam, in-memory dev store, transactions, batch, DSN hotload, filtering, and aggregate roots. Storage shape is per-service (SHAPES.md). |
events |
Domain events via a transactional outbox β Publisher/Bus/idempotency, an in-memory bus, and a Kafka bus, for safe cross-aggregate reactions. |
secret |
Secret-at-rest Encryptor: AES-256-GCM + HMAC for dev, HashiCorp Vault Transit for prod. |
lro |
AIP-151/152 long-running operations: Store, Manager, Operation, cancellation. |
cells |
Cell-based routing (isolation, not load balancing): a tenantβcell RoutingTable + watch-fed Router with a fail-safe default cell, an epoch-fenced GateRegistry admission barrier, and gRPC/HTTP middleware that rejects calls for a tenant mid-move. |
seccheck |
Static + dynamic security assertions you run in CI (see Security model). |
Full API reference for every package: Reference docs β.
The proto is the single source of truth; make generate (or buf generate) drives these:
Plugin (cmd/β¦) |
Output |
|---|---|
protoc-gen-svc |
service scaffold (*.svc.go) |
protoc-gen-storage |
GORM-backed Repository (*.storage.go) |
protoc-gen-ent |
ent schema (ent/schema/*.go) |
protoc-gen-devedge-authz |
the <Service>AuthzRules []MethodRule table (*.authz.go) |
Security is a foundation property here, not a bolt-on: it is enforced by construction and verified in CI, so a correctly-built service is secure by default.
Authorization is fail-closed: an undeclared or ungranted method is denied with no code required.
The seccheck package turns the model's invariants into assertions you run in CI (make security-check):
| Invariant | Asserted by |
|---|---|
Every RPC has (infoblox.authz.v1.rule) or public: true |
AssertRulesComplete |
| No secret-annotated field value in a read/list response | AssertNoSecretFieldsLeaked |
Unknown principal β PermissionDenied on all RPCs |
AssertUnknownPrincipalDenied |
| Account A cannot read Account B's resources | AssertCrossAccountIsolation |
| Errors never leak SQL, stack traces, or hostnames | AssertErrorMessagesClean |
β Security Check guide.
Secret fields. Mark a field secret in proto; generated code hashes it for lookup and encrypts the
ciphertext β AES-256-GCM in dev, HashiCorp Vault Transit in production β so plaintext is never persisted
and never returned. See model a resource β secret fields
and the Vault Transit guide.
WithAuthorizer / Config.Authorizer takes any authz.Authorizer. To target a production engine,
implement the one-method interface β an OPA-backed authorizer calling a sidecar, a Cedar/OpenFGA
client, a remote PDP β and pass it in. Nothing else in the service changes. The SDK core stays
engine-neutral: no OPA, no ORM, no policy-model types β those belong in adapters built on the SDK.
This repo is a multi-module workspace (WS-011 / F039): the dep-light root library plus six
nested modules (cmd, config/koanf, events/kafkabus, observability/otel, persistence/gormtx,
persistence/entrepo). A committed go.work resolves the cross-module references locally; the
build/vet/test targets loop over every module (the MODULES list in the Makefile).
make build # build every module (via go.work)
make test # unit tests across every module
make vet # go vet across every module
make lint # golangci-lint if installed, else go vet
make build-gowork-off # build each module with the workspace OFF (real requires only)
make check-graph-isolation # prove a server-only consumer's graph is free of the heavy adapter deps
make generate # rebuild plugins + regenerate after any .proto change
make security-check # run the seccheck assertions
make release VERSION=vX.Y.Z # synchronized multi-module release (dry run; PUSH=1 to publish)testdata/toy, testdata/apikey, testdata/fleet, and testdata/iam are separate consumer modules
with their own integration tests, deliberately NOT in go.work β run them with
cd testdata/<name> && GOWORK=off go test ./.... To carve a future heavy component into its own
isolated module, follow the Adding an Isolated Module
checklist. See AGENTS.md for the full contributor guide.
Apache-2.0. See LICENSE.