Guides

Creating a Go App

Step-by-step guide to building a backend application using the redelay go-framework.

This guide walks through building a production-ready Go backend using the redelay go-framework. The framework provides built-in modules for auth (JWT + passwords), users (CRUD + model), and groups (permissions), plus a server package that wires everything together automatically.

Prerequisites

  • Go 1.25+ (the framework modules declare go 1.25.0)
  • MongoDB 7+
  • (Optional) Redis 7+
  • A GitHub account with access to the redelay organization, and SSH set up for GitHub — the Redelay repos are currently private (see Step 1)

Project structure

A minimal redelay app is just a main.go:

text
myapp/
├── cmd/api/main.go      # Entry point (~20 lines)
├── go.mod
├── .env.example
└── Dockerfile

Key principle: framework modules own business logic AND HTTP routes; your app just includes them.

Step 1 — Initialize the module

shell
mkdir myapp && cd myapp
go mod init github.com/yourorg/myapp

Redelay's Go modules live in private GitHub repos and are consumed by version — go.mod pins tags, with no replace directives. Configure access once per machine:

shell
go env -w GOPRIVATE='github.com/redelay/*,github.com/flowdsl/*,github.com/FlowDSL/*,github.com/dreplyai/*'
git config --global url."[email protected]:".insteadOf "https://github.com/"

The repos are private, so this only works with a GitHub account that has been granted access and an SSH key registered with GitHub. Check before going further — this must print a commit hash, not a permission error:

shell
git ls-remote [email protected]:redelay/go-framework.git HEAD

Then require the framework and go mod tidy. The versions below are the working set the Redelay reference backend currently pins; @latest works too:

shell
go get github.com/redelay/[email protected]
go get github.com/redelay/[email protected]          # event bus + transports
# add-ons, as you need them:
go get github.com/redelay/[email protected]
go get github.com/redelay/[email protected]
go get github.com/redelay/[email protected]
go get github.com/redelay/[email protected]
go mod tidy

Local dev against a sibling checkout — the go.work override. When you edit the framework and your app together, don't add replace directives to go.mod (that's the legacy "Mode A" and it leaks into every clone). Instead use a gitignored go.work — the Go-modules analog of the admin's gitignored pnpm-workspace.yaml override (see the admin version):

text
// go.work  (gitignored; commit a go.work.example template instead)
go 1.25.0
use (
    .
    ../../redelay/go-framework
    ../../redelay/go-modules
    // … each sibling redelay module you're editing
)

go.mod stays authoritative (a fresh clone / CI builds against the pinned versions); the workspace overrides those with the local trees for dev. Never commit go.work.sum — a sum generated by one Go toolchain makes a container on a different toolchain misresolve a module and attempt a network fetch.

Containers with no GitHub credentials. Workspace mode still reads each pinned module's go.mod to compute the build graph, so a dev container (which can't fetch the private repos) needs those go.mods available. Mount the host module cache read-only and let the bind-mounted trees supply the source:

yaml
environment:
  GOFLAGS: -mod=readonly      # workspace mode forbids -mod=mod
  GOTOOLCHAIN: local          # don't chase a newer toolchain
  GOMODCACHE: /gomodcache
volumes:
  - ${HOST_GOMODCACHE}:/gomodcache:ro   # `go env GOMODCACHE` on the host

Full recipe (Makefile workspace/deps targets, the mod-cache mount, the never-commit-go.work.sum gotcha): the starter kit scaffolds all of it with create-redelay-app.sh (Mode B is the default) — see the starter kit.

Shipping a Mode B app — Dockerfile with SSH-forwarded private modules, swarm stack, push-to-deploy workflow — is covered in Deploying to production. The Redelay reference backend itself runs this way.

Legacy Mode A — replace github.com/redelay/go-framework => ../go-framework — still works for hacking inside the monorepo, but a fresh clone / CI can't build without the sibling trees and nothing is version-pinned. Prefer Mode B for any real project.

Step 2 — Create main.go

Create cmd/api/main.go:

go
package main

import (
    "log"

    // Blank imports trigger init() factory registration
    _ "github.com/redelay/go-framework/modules/asyncapi"
    _ "github.com/redelay/go-framework/modules/auth"
    _ "github.com/redelay/go-framework/modules/groups"
    _ "github.com/redelay/go-framework/modules/health"
    _ "github.com/redelay/go-framework/modules/mcp"
    _ "github.com/redelay/go-framework/modules/modspec"
    _ "github.com/redelay/go-framework/modules/openapi"
    _ "github.com/redelay/go-framework/modules/users"

    "github.com/redelay/go-framework/app"
    "github.com/redelay/go-framework/server"
)

func main() {
    inst, srv, err := server.Default()
    if err != nil {
        log.Fatal(err)
    }
    if err := app.Run(inst, srv); err != nil {
        log.Fatal(err)
    }
}

That's it. server.Default() bootstraps the app (connects MongoDB, discovers and starts modules) and creates an HTTP runner. app.Run() starts the runner and handles signals.

The HTTP runner sets up:

  • CORS middleware
  • JWT parsing (OptionalAuth)
  • Health check at {prefix}/health
  • All module routes (auth, users, etc.)
  • OpenAPI spec at /openapi.json + API reference UI at /reference
  • AsyncAPI spec at /asyncapi.json + event browser at /asyncapi
  • Module spec browser at /modules (+ /modules.json)
  • MCP server at /mcp

How auto-discovery works

  1. Each framework module registers a factory in its init() function (e.g. modules.RegisterFactory("auth", ...))
  2. Blank imports (_ ".../modules/auth") cause init() to run
  3. app.Bootstrap() calls DiscoverAndRegister() → all factories run, Configure phase resolves cross-dependencies (auth discovers users as its LookupProvider, groups as PermissionsProvider)
  4. Start() topologically sorts modules by DependsOn and starts them in order
  5. The HTTP runner iterates all modules implementing RoutesProvider, registering their routes

Multiple transports

The framework is transport-agnostic. The same modules can power HTTP, Kafka consumers, FlowDSL workers, gRPC servers, or WebSocket servers.

Wire an EventBus so all modules can publish and consume events. The recommended way is auto-wiring: blank-import the go-events lifecycle module and pick the transport with the TRANSPORT env var:

go
import (
    _ "github.com/redelay/go-events/module" // creates the bus from env and injects it into ModuleDeps
)
env
TRANSPORT=memory   # memory | kafka | nats | redis  (default: kafka)

memory needs no broker — use it for local development and tests. The lifecycle module registers every module's consumers during Configure and starts the bus during Startup, so do not call bus.Start yourself — eventbus.EventBus ignores consumers registered after Start, and calling it before app.Bootstrap silently drops every module consumer.

To supply your own transport instead, pass the bus with app.WithEventBus and keep the blank import — the lifecycle module then registers consumers on, and starts, the bus you passed:

go
import (
    "github.com/redelay/go-events/eventbus"
    "github.com/redelay/go-events/transport/kafka"
    _ "github.com/redelay/go-events/module"
)

t, err := kafka.NewFromEnv() // reads KAFKA_BOOTSTRAP_SERVERS etc.
if err != nil { log.Fatal(err) }
bus := eventbus.New(t)

inst, err := app.Bootstrap(app.WithEventBus(bus)) // no bus.Start — Startup does it

See go-events for the transport env vars.

Modules receive ModuleDeps.EventBus and call bus.Publish(ctx, msg) — they never import Kafka or Redis directly. See Transports for the full API.

Step 3 — Configure environment

Create .env.example:

env
HOST=0.0.0.0
PORT=8000
ENV=development
ROUTE_PREFIX=/api/v1
MONGO_URI=mongodb://localhost:27017
MONGO_DATABASE=myapp
CORS_ORIGINS=http://localhost:3000
AUTH_JWT_SECRET=change-me-in-production
TRANSPORT=memory   # only read when go-events/module is imported

Step 4 — Adding custom routes

Use srv.Router() to access the underlying chi router and add custom endpoints:

go
func main() {
    inst, srv, err := server.Default()
    if err != nil {
        log.Fatal(err)
    }

    // Add a custom route
    srv.Router().Get("/api/v1/ping", func(w http.ResponseWriter, r *http.Request) {
        httputil.WriteJSON(w, http.StatusOK, map[string]string{"pong": "ok"})
    })

    log.Fatal(app.Run(inst, srv))
}

Step 5 — Advanced: full control

For cases where you need full control over the app lifecycle:

go
package main

import (
    "context"
    "log"
    "time"

    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "go.uber.org/zap"

    "github.com/redelay/go-framework/app"
    "github.com/redelay/go-framework/modules"
    "github.com/redelay/go-framework/server"

    _ "github.com/redelay/go-framework/modules/auth"
    _ "github.com/redelay/go-framework/modules/groups"
    _ "github.com/redelay/go-framework/modules/users"
)

func main() {
    logger, _ := zap.NewDevelopment()

    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    client, _ := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
    cancel()

    application := app.New("myapp", logger)
    application.DiscoverAndRegister(modules.ModuleDeps{
        DB:     client.Database("myapp"),
        Logger: logger,
    })
    application.Start(context.Background())

    inst := &app.Instance{App: application, MongoClient: client, Logger: logger}
    srv := server.FromInstance(inst, server.LoadConfig())
    // Add custom routes, middleware, etc.
    log.Fatal(app.Run(inst, srv))
}

Module routes

Built-in modules expose their routes automatically via RoutesProvider:

auth module routes (under /auth)

  • POST /login — Login with email + password (also supports OAuth2 form username/password)
  • POST /refresh — Refresh token pair
  • POST /revoke — Revoke a refresh token

users module routes (under /users)

  • GET /me — Current user profile (authenticated)
  • GET / — List users (superuser only)
  • POST / — Create user (superuser only)
  • GET /{id} — Get user by ID (self or superuser)
  • PATCH /{id} — Update user (self or superuser; non-superusers cannot change is_active/is_superuser)
  • DELETE /{id} — Delete user (superuser only)

All routes are served under the configured ROUTE_PREFIX (default /api/v1), so the full path for login is POST /api/v1/auth/login.

Built-in middleware

The auth module provides net/http middleware you can use with chi:

MiddlewareDescription
auth.AuthMiddleware(jwtCfg)Require valid JWT; rejects unauthenticated requests
auth.OptionalAuth(jwtCfg)Parse JWT if present, continue if absent
auth.RequireActiveUserEnsure claims exist in context
auth.RequireSuperuserRequire is_superuser == true
auth.RequirePermission("perm")Check specific permission (superusers bypass)
auth.RequireAnyPermission("a","b")Check any of the listed permissions
auth.APISecretMiddleware(secret)Validate X-API-Secret header

Permissions

Permissions are resolved at login/refresh time from the user's groups and embedded in the JWT:

  1. User model has user_groups []ObjectID
  2. Groups module stores permissions []string per group
  3. At login, auth module calls PermissionsResolver.ResolvePermissions(ctx, groupIDs) → gets union of all group permissions
  4. Permissions are embedded in Claims.Permissions
  5. RequirePermission("users:write") checks Claims.Permissions — O(1) per request, no DB lookup

Environment variables

Bootstrap (all transports)

VariableDefaultDescription
APP_NAMEredelayApplication name
ENVdevelopmentEnvironment name
DEBUGfalseDebug mode
MONGO_URImongodb://localhost:27017MongoDB connection string
MONGO_DATABASEredelayDatabase name
MONGO_TIMEOUT_SECONDS15Connection timeout

HTTP server

VariableDefaultDescription
HOST0.0.0.0Listen host
PORT8000Listen port
ROUTE_PREFIX/api/v1API route prefix
CORS_ORIGINS*Comma-separated allowed origins

Auth module

VariableDefaultDescription
AUTH_JWT_SECRETchange-meJWT signing secret
AUTH_JWT_ALGORITHMHS256JWT algorithm (HS256/HS384/HS512)
AUTH_ACCESS_TOKEN_TTL_MINUTES30Access token lifetime
AUTH_REFRESH_TOKEN_TTL_DAYS7Refresh token lifetime

Users module

VariableDefaultDescription
USERS_COLLECTIONusersMongoDB collection name
USERS_DEFAULT_PAGE_SIZE20Default pagination size
USERS_MAX_PAGE_SIZE100Maximum pagination size

Groups module

VariableDefaultDescription
GROUPS_COLLECTIONgroupsMongoDB collection name

Local development

The app needs MongoDB (and Redis only if you use Redis-backed features). A minimal docker-compose.yml next to go.mod:

yaml
services:
  mongo:
    image: mongo:7
    ports: ["27017:27017"]
    volumes: [mongo-data:/data/db]
  redis:            # optional
    image: redis:7
    ports: ["6379:6379"]
volumes:
  mongo-data:
shell
docker compose up -d        # start dependencies
cp .env.example .env        # then export it, e.g. `set -a; . ./.env; set +a`
go run ./cmd/api            # run the app

The starter kit scaffolds the same thing (infra/docker-compose.yml, plus a Mongo-only docker-compose.min.yml meant to pair with TRANSPORT=memory).

Available endpoints

MethodPathDescription
GET/api/v1/healthHealth check
POST/api/v1/auth/loginLogin (email + password, or OAuth2 form)
POST/api/v1/auth/refreshRefresh token pair
POST/api/v1/auth/revokeRevoke refresh token
GET/api/v1/users/meCurrent user profile
GET/api/v1/usersList users (admin)
POST/api/v1/usersCreate user (admin)
GET/api/v1/users/{id}Get user by ID
PATCH/api/v1/users/{id}Update user
DELETE/api/v1/users/{id}Delete user (admin)
GET/openapi.jsonOpenAPI 3.0 spec
GET/referenceAPI reference UI (Scalar/Swagger/Redoc)
GET/asyncapi.jsonAsyncAPI 2.6 spec (JSON)
GET/asyncapiAsyncAPI Studio — event browser UI
GET/modules.jsonModule spec JSON (routes, events, consumers per module)
GET/modulesModule spec browser (links to OpenAPI + AsyncAPI)
POST/mcpMCP JSON-RPC endpoint

POST /api/v1/users/signup is not in this list: signup is flow-driven and is only bound when the app also imports flowexec (go-flowdsl/flowexec/module) and go-framework/modules/users/flowtemplates, which auto-publishes the users/registration-basic flow on first boot.