AnnouncementsIntroducing Arkoren

    Menu
    Introducing Arkoren Arkoren's wordmark and stepped Strata A on a subtle rose grid. INTRODUCING Arkoren
    ← All posts
    Announcements

    Introducing Arkoren

    10 min readUpdated Nov 3, 2026

    Why Arkoren brings Go libraries together around shared conventions for input, data, background work, and application lifecycle—without requiring you to adopt the whole toolkit.

    Erik C. ForésSr. Cloud Technical Lead at HP
    On this page

    Go gives you room to build an application on your own terms. You can choose a router, write SQL, use a queue where it helps, and keep the rest in ordinary Go. I want to keep that freedom.

    But choosing the libraries is only part of the work. The application still has to connect them: turn failures into useful HTTP responses, carry a trace from a request into a database call, start workers after their dependencies, and stop those workers before closing the connections they use. Those decisions matter, even when none of them is the feature you set out to build.

    Arkoren is an open-source Go toolkit for building and running applications. Its libraries share conventions for configuration, errors, telemetry, and lifecycle management, so you have less connecting code to maintain. You can use them together or adopt one package without replacing your router or reorganizing your project.

    The aim is less repeated plumbing, not less control. Your application still decides what runs, what it depends on, and who is allowed to do what. Arkoren provides reusable pieces for expressing those decisions and seeing what happens when they run.

    A note on the repo’s age: The fresh Git history doesn’t reflect the project’s full history. Arkoren evolved from Cosmos over the past two years. It changed enough in scope and design that I chose to give it a new home and a clean history.

    Publishing an Article

    Consider an application where authors write drafts and publish articles. The first task is straightforward: accept a title and save a draft. Publishing later adds search indexing, perhaps a notification, and eventually the question of why one request took two seconds.

    That feature gives us a way to look at the connections. The examples below are focused fragments, not a complete service. Existing collaborators are named where they are needed; error results remain visible, but routine propagation is left to the linked guides.

    Validating Input

    A title made only of spaces should not become a draft. The same rule should apply whether the draft arrives through HTTP, an import, or an internal tool.

    Start with the fields a caller is allowed to supply. The database generates the ID, and the application—not an arbitrary input field—controls publication:

    type ArticleInput struct {
    	Title string `json:"title" db:"title"`
    }

    With validate imported from arkoren.dev/foundation/validate, declare the title’s rules once:

    var articleInput = validate.Struct(func(object *validate.StructBuilder[ArticleInput]) {
    	object.Field("Title", func(title *validate.StringBuilder[string]) {
    		title.Trim()
    		title.Required()
    		title.Max(200)
    	})
    })

    The schema trims surrounding whitespace, then checks the title. A value of " Hello " becomes "Hello"; a whitespace-only value fails the required rule. The schema can run without an HTTP request, so another entry point can reuse the same normalization and validation.

    In a handler, request.ParseJSON connects JSON decoding to those rules. Here r is the incoming request and request comes from arkoren.dev/core/http/request:

    input, err := request.ParseJSON(r, articleInput)

    Check the error before storing anything. Returning the validation failure to Arkoren’s router produces a 422 response using Problem Details. Its type identifies the problem, and errors identifies the invalid field:

    {
    	"type": "tag:arkoren.dev,2026-09:core:http/request:validation-failed",
    	"title": "Validation failed",
    	"status": 422,
    	"errors": {
    		"/title": [
    			{
    				"code": "required",
    				"message": "is required"
    			}
    		]
    	}
    }

    Clients can recognize a problem without parsing a human-readable message. Application-defined failures, such as a missing article or a conflict, use the same HTTP format. Internal causes remain available to server-side diagnostics without becoming part of the client response.

    Saving Articles

    With valid input, the next step is a database write. Assume a started PostgreSQL or SQLite database facade named db and an articles table with a generated id, a title, and a status that defaults to draft.

    Use query from arkoren.dev/foundation/query to describe the insert:

    insert := query.Table("articles", func(statement *query.Builder) {
    	statement.Insert(query.Values(input))
    	statement.Returning("id")
    })

    query.Values maps the input’s db tag to the writable column. The title is passed as a parameter, not interpolated into SQL. The database facade executes the statement and reads the generated ID:

    id, err := db.Find[int64](r.Context(), insert)

    This creates a draft; it does not authorize or perform publication. Your application owns those decisions. The database guide covers execution and result mapping, while Queries covers building statements that you can also use with existing database code. There is no required ORM or generated model type.

    Evolving the Schema

    The table also needs to reach every environment. A schema definition makes its intended shape reviewable alongside the application code. Using arkoren.dev/foundation/query/schema:

    articles := schema.Create("articles", func(table *schema.Table) {
    	table.ID()
    	table.String("title", 200)
    	table.String("status", 20).Default("draft")
    })

    For supported operations, the builder handles SQL differences between PostgreSQL, MySQL, and SQLite. Wrap the definition in a named migration with migrate from arkoren.dev/core/service/database/migrate:

    createArticles := migrate.NewMigration("create-articles", func(migration *migrate.MigrationBuilder) {
    	migration.Up(migrate.Schema(articles))
    })

    The migrator applies registered pending migrations in order and records successful application. Migrators sharing the same database and lock identity coordinate their runs. When the table needs an author column, append a new migration instead of changing the recorded one.

    That history is not a promise that every schema change can be undone or retried without inspection. Transaction support and partially applied changes depend on the database and operation. The migration guides explain deployment and recovery; the application chooses when to run migrations.

    Background Work

    Creating a draft is not publishing it. Later, an authorized publishing operation changes its status, and readers need the search index to catch up. The author should not have to wait for the indexing service to finish.

    A queue separates accepting that work from executing it. With queue imported from arkoren.dev/core/service/queue, define the payload and job:

    type IndexArticle struct {
    	ArticleID int64 `json:"article_id"`
    }
    
    var IndexArticleJob = queue.NewJob[IndexArticle]("index-article", func(job *queue.JobBuilder) {
    	job.Retry(3)
    })

    After the publication write succeeds, dispatch the article’s ID through a configured queue dispatcher named dispatcher, using the operation context ctx:

    err := dispatcher.Dispatch(ctx, IndexArticleJob, IndexArticle{ArticleID: id})

    Successful dispatch means the driver accepted the job, not that indexing finished. Durability and redelivery depend on the driver; an in-memory queue does not survive a restart. The database write and dispatch are separate operations. If the handoff must survive a crash between them, your application needs a durable handoff strategy.

    A consumer connected to the same backend registers the work. Here search is your application’s indexing service, with an Index(context.Context, int64) error method—not Arkoren’s search facade:

    err := consumer.Handle(IndexArticleJob, func(ctx context.Context, job IndexArticle) error {
    	return search.Index(ctx, job.ArticleID)
    })

    The job requests at most three total attempts for ordinary handler failures when the driver reports attempt counts; otherwise the backend’s redelivery policy controls the limit. Design indexing to tolerate repeated delivery. The queue guides cover worker lifetime, failure handling, and backend differences.

    Not every reaction needs a queue. For local reactions, an event bus lets publishing code notify listeners without naming each one. Those listeners run synchronously and are not durable: they separate responsibilities, but do not move work out of the request.

    Running the Service

    The handler and worker depend on resources that must outlive their work. Starting a database before a worker is one half of that relationship; keeping the database open while the worker stops is the other.

    Arkoren’s application coordinator gives those relationships an explicit place. Inside a composition callback, you register startup, supervised work, and cleanup. For an existing router named routes and a cancellation context named ctx, an HTTP listener can be registered like this:

    app := application.New()
    err := app.Run(ctx, func(compose *application.Composer) {
    	listener := server.New(routes)
    	compose.Cleanup(listener.Stop)
    	compose.Startup(listener.Start)
    	compose.Run(listener.Serve)
    })

    This fragment uses arkoren.dev/core/application and arkoren.dev/core/http/server. Register dependency startup before the work that needs it, and cleanup as soon as you own a resource. All composition callbacks finish before startup begins. Registered cleanup also runs if a later composition or startup step fails.

    On shutdown, the application cancels its runners and waits within its shutdown budget, then attempts cleanup in reverse registration order and finalizes telemetry. Cancellation is cooperative: a runner that ignores its context can outlive that wait and overlap cleanup. The lifecycle guide explains those boundaries; registration does not infer dependencies or forcibly stop code.

    Readiness is a separate decision. Register checks for the dependencies that must work before traffic arrives, rather than treating a running process as proof that it can serve requests.

    Following a Request

    Now suppose publishing sometimes takes two seconds. A request log gives you the duration, but not whether the delay was in SQL, queue dispatch, or an upstream call.

    With request tracing enabled, instrumented components using the same telemetry handle and request context can contribute spans to that request’s trace. You can follow the time across those operations without adding a timer around every call yourself. Metrics help establish whether the delay is isolated or recurring across requests.

    Telemetry can be exported through OpenTelemetry or exposed as Prometheus metrics, according to the driver you configure. For local investigation, bounded in-memory capture provides recent traces and logs without requiring an external tracing backend.

    Publish the diagnostic tools explicitly on a protected MCP endpoint, then connect the Arkoren CLI. These tools expose application internals; a loopback address does not replace authentication and authorization.

    For example, assume the application publishes capture tools with prefix app. With default response correlation enabled, the traceresponse HTTP header contains 00-<trace-id>-<span-id>-<flags>. Set TRACE_ID to its 32-hex trace-ID portion—not the entire header—and inspect the retained trace:

    arkoren app:trace "$TRACE_ID"

    The result joins finished retained spans with captured logs, helping distinguish a slow insert from a slow dispatch. Capture is bounded: missing evidence can mean eviction or a different process, not that an operation never ran. The diagnosis guide connects trace inspection, health, and readiness into an investigation workflow.

    Beyond This Example

    The publishing feature uses only part of Arkoren. Other service contracts cover caching, file storage, mail, payments, workflows, and language models.

    A service contract defines the portable API and a conformance suite for its drivers. Driver modules connect that API to particular infrastructure. Your calling code can keep the same facade, but switching drivers still requires checking capabilities, durability, and operational requirements. Each service’s Drivers page documents those differences in a compatibility matrix.

    The HTTP packages cover routing, authentication, authorization, sessions, signed links, and middleware. The foundation libraries—including queries, validation, collections, and Problem Details—import only the standard library and work without core. These are separate adoption choices, not a required stack.

    Trying Arkoren

    You need Go 1.27 or later. In an existing application’s module directory, install core and import the packages you need:

    go get arkoren.dev/core

    Keep your existing entry point, router, and layout. Foundation libraries can be installed independently, and vendor drivers live in their own modules. The installation guide covers choosing the relevant module.

    For a new service, install the CLI and generate a starter:

    go install arkoren.dev/cmd/arkoren@latest
    arkoren new hello --module example.com/hello

    The starter wires an HTTP service with configuration, health probes, and lifecycle management. It creates no application tests and does not resolve its dependencies. Follow Creating Projects before building, then use the quickstart to run the service and add a route, a cache counter, and a checked error response.

    Arkoren is pre-1.0. Modules version independently, and v0 minor releases can break APIs. Security fixes target the latest version of each affected module, without guaranteed backports. Read the compatibility and support policy, commit your resolved go.mod and go.sum, and run your application’s tests before deploying an upgrade. Start with a capability you need, then decide whether another piece earns its place.

    What Comes Next

    The work toward 1.0 is about refining the APIs already here: learning from real applications, measuring hot paths, reducing allocations, and removing rough edges before adding more surface area.

    There are no built-in arks yet. Those opt-in modules are intended for domain features built on the underlying contracts, rather than another requirement for using them. They are a direction for the toolkit, not an installable part of it today.

    Acknowledgments

    Laravel is an important influence. Taylor Otwell and the Laravel community showed how much libraries can give back when their pieces are designed together, and how readable query, schema, and validation builders can be. Arkoren brings that inspiration to a different language, with Go’s explicit composition and ordinary types.

    Thanks also to the OpenTelemetry project, the authors of RFC 9457, and the Go team. Their standards and libraries underpin much of what this toolkit connects.

    Get Involved

    I’m especially interested in feedback from developers building HTTP services in Go: which parts fit your application, where the boundaries feel awkward, and what you still have to wire yourself.

    Try one package through the installation guide, explore the source, or open an issue with what you find. I’d love to see what you build with it.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive