Skip to main content

Coding Patterns

This guide covers the core patterns and conventions used across the Crawbl orchestrator codebase.

Session-Per-Request

Every HTTP request creates one *dbr.Session. That session flows from handler to service to repo.

func (s *Server) handleUsersProfile(w http.ResponseWriter, r *http.Request) {
user, merr := s.authService.GetBySubject(r.Context(), &orchestratorservice.GetUserBySubjectOpts{
Sess: s.newSession(),
Subject: principal.Subject,
})
if merr != nil {
httpserver.WriteErrorResponse(w, httpStatusForError(merr), merrors.PublicMessage(merr))
return
}

httpserver.WriteSuccessResponse(w, http.StatusOK, toUserProfileResponse(user))
}

For multi-step mutations that must succeed or fail together, wrap them in database.WithTransaction:

result, merr := database.WithTransaction(sess, "create workspace", func(tx *dbr.Tx) (*orchestrator.Workspace, *merrors.Error) {
ws, merr := s.workspaceRepo.Create(ctx, tx, workspace)
if merr != nil {
return nil, merr
}
if merr := s.agentRepo.CreateDefaults(ctx, tx, ws.ID); merr != nil {
return nil, merr
}
return ws, nil
})

WithTransaction takes:

  • a *dbr.Session
  • an operation name used in wrapped error messages
  • a callback returning (T, *merrors.Error)

The transaction commits on success and rolls back on error automatically.

Typed Option Structs

Service methods accept a single opts struct instead of positional arguments. This keeps signatures stable as fields are added.

type ListWorkspacesOpts struct {
Sess *dbr.Session
UserID string
}

func (s *service) ListByUserID(ctx context.Context, opts *ListWorkspacesOpts) ([]*orchestrator.Workspace, *merrors.Error) {
// ...
}

Rules:

  • Include Sess *dbr.Session in request-scoped service opts
  • Define opts structs in the service package unless they are shared more broadly
  • Use {Action}{Entity}Opts naming

Error Handling

The codebase uses structured *errors.Error values rather than raw error for request-path logic.

Business Errors

Safe to return to clients:

return nil, merrors.NewBusinessError("User not found", merrors.ErrCodeUserNotFound)

Server Errors

Internal failures are wrapped and logged server-side:

return nil, merrors.WrapStdServerError(err, "load workspace")

Error Code Convention

PrefixDomain
AUTHAuthentication and authorization
USRUser operations
WSPWorkspace operations
AGTAgent operations
CHTConversation operations
MSGMessage operations
RTMRuntime lifecycle
INTIntegrations

Examples: AUTH0001, USR0002, WSP0001, CHT0001.

Define new codes in internal/pkg/errors/types.go.

Configuration

ConfigFromEnv

Common packages expose ConfigFromEnv(prefix) helpers. For example:

cfg := database.ConfigFromEnv("CRAWBL_")
// Reads CRAWBL_DATABASE_HOST, CRAWBL_DATABASE_PORT, etc.

SecretString

For secrets that may be provided either directly or via *_FILE, use configenv.SecretString:

value := configenv.SecretString("CRAWBL_DATABASE_PASSWORD", "")
// Reads CRAWBL_DATABASE_PASSWORD
// Falls back to CRAWBL_DATABASE_PASSWORD_FILE

Rules:

  • No hardcoded credentials
  • Use env vars or *_FILE mounts for secrets
  • Keep secret loading close to config construction

Naming Conventions

Package Names

TypeConventionExample
Repository{entity}repouserrepo, agentrepo, conversationrepo
Service{domain}serviceauthservice, chatservice, workspaceservice

File Names

  • types.go for shared structs, interfaces, and constants
  • Handler files grouped by area, such as handle_auth.go, handle_workspace.go, handle_chat.go
  • One implementation file per small repo or service subpackage when practical

Database

  • Table names: plural, lowercase
  • Column names: snake_case
  • Timestamps: TIMESTAMPTZ
  • Foreign keys: explicit REFERENCES with clear delete behavior

What's Next

See Running Tests.