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.Sessionin request-scoped service opts - Define opts structs in the service package unless they are shared more broadly
- Use
{Action}{Entity}Optsnaming
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
| Prefix | Domain |
|---|---|
AUTH | Authentication and authorization |
USR | User operations |
WSP | Workspace operations |
AGT | Agent operations |
CHT | Conversation operations |
MSG | Message operations |
RTM | Runtime lifecycle |
INT | Integrations |
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
*_FILEmounts for secrets - Keep secret loading close to config construction
Naming Conventions
Package Names
| Type | Convention | Example |
|---|---|---|
| Repository | {entity}repo | userrepo, agentrepo, conversationrepo |
| Service | {domain}service | authservice, chatservice, workspaceservice |
File Names
types.gofor 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
REFERENCESwith clear delete behavior
What's Next
See Running Tests.