Todoers is a full-featured, multi-list task management application built to demonstrate the capabilities and design paradigms of the Rays framework by BeamFoundry.
Rays brings proven enterprise architectural patterns — reminiscent of Spring Boot and Java EE — natively into Go. It provides an Inversion of Control (IoC) container, declarative Stereotype scanning, compile-time validation, automatic dependency injection, profile-based configuration, Spring Data-style ORM repositories, and declarative web security.
- Features
- Architecture & Tech Stack
- Core Rays Concepts Demonstrated
- Project Structure
- Getting Started
- Configuration & CLI Overrides
- Code Walkthrough
- License
- Multi-List Kanban Board: Create, rename, delete, and manage multiple todo lists.
- Drag-and-Drop Reordering: Interactive reordering of lists and moving items across lists powered by Sortable.js and HTMX.
- Inline Editing & Instant Updates: Dynamic CRUD operations without full page reloads.
- Authentication & Authorization:
- Secure form-based authentication with CSRF protection.
- Persistent "Remember Me" token authentication.
- Role-Based Access Control (
ROLE_USER,ROLE_ADMIN). - Automatic admin promotion: the first registered user is automatically granted
ROLE_ADMIN.
- User Profile Management: Update display names and change passwords with server-side validation.
- Admin Dashboard: Live search of registered users with pagination (
ROLE_ADMINonly). - Internationalization (i18n): Instant switching between English (
en) and French (fr) with localized messages and validation feedback. - Cache-Busted Static Assets: Asset hashing via
hashfsfor seamless client-side caching. - Pure Go & CGO Dual Support: Configured to run either completely without CGO (
glebarez/sqlite) or with native CGO SQLite (mattn/go-sqlite3).
┌──────────────────────────────────────────────┐
│ Browser / Client │
│ (Bootstrap 5 + HTMX + Sortable.js + CSS) │
└──────────────────────┬───────────────────────┘
│ HTTP / REST / HTML Fragments
▼
┌──────────────────────────────────────────────┐
│ Fiber v3 Web Engine (rays-fiber) │
│ - FiberControllers & RequestMapping │
│ - HttpSecurity Filters & SecurityContext │
│ - Error Handler & Locale Resolver │
└──────────────────────┬───────────────────────┘
│ Dependency Injection (@:"Inject")
▼
┌──────────────────────────────────────────────┐
│ Services & Business Logic │
│ (TodoService, UserService, etc.) │
│ - Declarative GormTransactional Tx │
└──────────────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ ORM Layer (rays-orm + GORM) │
│ - Generic Repositories: OrmRepository[T,ID]│
│ - Method-name derived queries │
└──────────────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ SQLite Database File │
│ (Auto-migrated by DbMigrator at startup) │
└──────────────────────────────────────────────┘
- Core Framework: BeamFoundry/rays
- Web Layer: BeamFoundry/rays-fiber on top of Fiber v3
- Data Layer: BeamFoundry/rays-orm on top of GORM
- Security: BeamFoundry/rays-web-security
- Templates: Go
html/templatewith Sprig functions - Static Asset Serving: HashFS for content-hashed asset URLs
In Rays, components managed by the IoC container are called Beams (analogous to Beans in Spring). Structs declare their role by embedding one of the framework's Stereotypes:
| Stereotype | Equivalent | Purpose |
|---|---|---|
Component |
@Component |
General-purpose managed component |
Service |
@Service |
Service layer holding business logic |
Repository |
@Repository |
Data access object interface / implementation |
Controller |
@Controller |
Web request handler / endpoint controller |
Configuration |
@Configuration |
Producer of Beams through factory methods |
ConfigurationProperties |
@ConfigurationProperties |
Binds external YAML/environment configuration to typed structs |
Rays provides a dedicated code generation tool:
go tool stereotyperThe tool scans the packages and generates zz_stereotype.go in each directory. These files contain compile-time interface assertions (e.g. var _, _ = any(&DbConfig{}).(_rays_core.Stereotype)), ensuring that all annotated structs correctly satisfy framework constraints at build time.
Dependencies are injected into struct fields or factory method arguments using the @:"Inject" struct tag:
type TodoServiceImpl struct {
Service
transactional GormTransactional `@:"Inject"`
todoRepository TodoRepository `@:"Inject"`
itemRepository ItemRepository `@:"Inject"`
messageSource MessageSource `@:"Inject"`
}Rays resolves and wires references automatically during ctx.Initialize().
Rays supports conditional Beam creation:
ConditionalOnMissingBeam: Instantiates a fallback Beam only if no other Beam of that type is registered.ConditionalOnProfile: Activates a Beam only when a specific profile (such astest) is enabled.ConditionalOnNoCGO/ConditionalOnCGO: Selects different implementations depending on whether the binary was compiled with CGO enabled or disabled.
For instance, in pkg/configurations/database.go, the application switches between native SQLite and pure-Go SQLite based on the build target:
func (this *DialectorConfig) DBOpener(_ struct { ConditionalOnMissingBeam }) DBOpener {
return &SQLiteDB{} // CGO SQLite
}
func (this *DialectorConfig) DBOpenerNoCGO(_ struct { ConditionalOnNoCGO }) DBOpener {
return &SQLiteDBNoCGO{} // Pure-Go SQLite
}Similar to Spring Data JPA, rays-orm provides generic repositories (OrmRepository[T, ID]) where query functions are derived automatically from the method names declared on the interface:
type ItemRepository interface {
OrmRepositoryInterface[Item, uint]
FindAllByTodoID(todoId uint) []Item
FindAllByTodoIDOrderByRank(todoId uint) []Item
FindAllByTodoIDAndRankGreaterThanOrEqual(uint, uint) []Item
FindOneByTodoIDAndItemID(uint, uint) Optional[*Item]
}No boilerplate SQL or GORM query chains need to be written by hand for standard queries.
Web security is configured in a fluent, expressive DSL using HttpSecurityBuilder:
func (this *SecurityConfig) HttpSecurity(that struct { rememberme RememberMeService `@:"Inject"` }) *HttpSecurity {
return (&HttpSecurityBuilder{}).
AuthorizedHTTPRequests().
Paths("/profile").Authenticated().
Paths("/todolists**").HasAuthority("ROLE_USER").
Paths("/admin", "/admin/**").HasAuthority("ROLE_ADMIN").
Paths("/static/**").PermitAll().
Paths("/*").PermitAll().
And().
AnyRequest().DenyAll().
FormLogin().
ViewName("views/login").
And().
RememberMe(that.rememberme).
Build()
}Controllers embed FiberController and use RequestMapping struct tags to define route patterns and HTTP methods:
type TodolistsController struct {
FiberController
RequestMapping `Path:"/todolists"`
AppSecure
todoService TodoService `@:"Inject"`
}
func (this *TodolistsController) ListAll(
_ struct { RequestMapping `Method:"GET"` },
c fiber.Ctx,
) error {
user := fiber.Locals[*User](c, "User")
return c.Render("views/fragments/todolist", fiber.Map{
"Todos": this.todoService.GetUserTodoLists(user),
}, "views/fragments/todoitem")
}Handlers return HTML template fragments that HTMX swaps directly into the DOM, giving a single-page app feel with zero JavaScript framework overhead.
Application messages and validation error translations are loaded from embedded YAML resource files (resources/config/translation/):
- Default locale:
en(English) - Alternate locale:
fr(French) - Switched via query parameter (e.g.
?lang=fror?lang=en) handled byLocaleResolver.
rays-sample-todoers/
├── cmd/
│ └── main.go # Bootstrap, CLI flags, configuration loading, container execution
├── internal/
│ ├── configurations/ # Configuration beams (DB, Fiber, Security, i18n, Sprig, Static FS)
│ ├── repositories/ # rays-orm derived repositories (TodoRepository, ItemRepository, UserRepository)
│ └── services/ # Business logic & transactions (TodoService, UserService)
├── pkg/
│ ├── controllers/ # FiberControllers (Todolists, Page, Register, Admin, Static)
│ │ └── handlers/ # Security & locale route middleware (AppSecure, AppSecureTodo, etc.)
│ ├── core/ # Services and Repository interfaces.
│ └── domains/ # Domain entities (User, Todo, Item)
├── resources/
│ ├── config/
│ │ ├── rays.yaml # Default application configuration (logging, server port, etc.)
│ │ └── translation/ # i18n translation bundles for en and fr
│ ├── static/ # Static assets: CSS, JavaScript, favicons
│ ├── views/ # HTML templates, layouts, and HTMX fragments
│ └── resources.go # Go embed.FS wrappers for resources
├── Makefile # Build targets (all, tool, cgo, nocgo)
├── todolist.db # Default SQLite database
├── go.mod & go.sum # Module definitions and dependencies
└── README.md # Project documentation
- Go: Version
1.26or later (or appropriate Go toolchain) - Make (optional, for running Makefile targets)
- GCC / C compiler (optional, only if building with CGO SQLite)
The project provides a Makefile with targets for both pure Go and CGO builds:
# Pure Go build (no CGO required, uses glebarez/sqlite)
make nocgo
# Alternatively, build with CGO enabled (uses mattn/go-sqlite3)
make cgoThe Makefile automatically runs go tool stereotyper before compiling the binary to ensure stereotype assertions are up to date.
To run tests:
go test ./...Start the compiled binary:
./todolistBy default, the server listens on http://localhost:3000.
- Navigate to
http://localhost:3000in your web browser. - Click Register to create an account.
Note: The first registered account is automatically assigned the
ROLE_ADMINauthority and has access to the/adminuser management panel. - Start creating todo lists, adding items, editing them, and dragging them around to reorder!
Configuration is driven by resources/config/rays.yaml embedded directly inside the binary. You can inspect or customize settings via command-line flags or environment variables without recompiling.
Usage of ./todolist:
-c, --config strings Additional configuration directory paths (io/fs.FS)
-p, --profile strings Profiles to activate (comma-separated or multiple flags)
--override stringToString Override configuration keys inline (key=value)Change the listening port to :8080:
./todolist --override rays.fiber.servers.primary.address=":8080"Activate the test profile (uses an in-memory SQLite database):
./todolist -p testSpecify additional configuration directories:
./todolist -c /etc/todoers/configRays automatically scans environment variables starting with RAYS_ or APP_. Variables are converted to lowercase and underscores are translated to dots:
# Overrides rays.fiber.servers.primary.address=":9000"
export RAYS_FIBER_SERVERS_PRIMARY_ADDRESS=":9000"
# Overrides rays.logging.level="DEBUG"
export RAYS_LOGGING_LEVEL="DEBUG"
./todolistConfigurations follow Spring Boot-style naming conventions:
rays.yaml— loaded by default.rays-<profile>.yaml— loaded when<profile>is active (e.g.--profile test).
Services embed Service and use @:"Inject" to declare dependencies. Declarative transactions are managed via GormTransactional:
package services
import (
"context"
. "github.com/BeamFoundry/rays/pkg/core"
. "github.com/BeamFoundry/rays-orm/pkg/core"
. "github.com/BeamFoundry/rays-sample-todoers/pkg/domains"
. "github.com/BeamFoundry/rays-sample-todoers/internal/repositories"
)
type TodoServiceImpl struct {
Service
transactional GormTransactional `@:"Inject"`
todoRepository TodoRepository `@:"Inject"`
itemRepository ItemRepository `@:"Inject"`
}
func (this *TodoServiceImpl) SaveList(todo *Todo) (*Todo, error) {
tx := this.transactional.Start(context.Background())
defer this.transactional.End()
t, err := this.todoRepository.Save(todo)
if err != nil {
tx.Rollback()
return nil, err
}
tx.Commit()
return t, nil
}Define your repository interface by embedding OrmRepositoryInterface[Model, IDType] and declaring query method signatures:
package repositories
import (
. "github.com/BeamFoundry/rays-orm/pkg/core"
. "github.com/BeamFoundry/rays-sample-todoers/pkg/domains"
. "github.com/BeamFoundry/rays/pkg/lang"
)
type UserRepository interface {
OrmRepositoryInterface[User, uint]
FindByUsername(string) Optional[*User]
FindAllUsersByUsername(string, ...OrmQueryFunc) []User
}
type UserRepositoryImpl struct {
OrmRepository[User, uint]
findByUsername func(string) Optional[*User]
findAllUsersByUsernameLikeOrderByID func(string, ...OrmQueryFunc) []User
}
func (this *UserRepositoryImpl) FindByUsername(username string) Optional[*User] {
return this.findByUsername(username)
}Use HttpSecurityBuilder inside a Configuration beam to secure URL patterns:
package configurations
import (
. "github.com/BeamFoundry/rays-web-security/pkg/config"
. "github.com/BeamFoundry/rays-web-security/pkg/core"
. "github.com/BeamFoundry/rays/pkg/core"
)
type SecurityConfig struct {
Configuration
}
func (this *SecurityConfig) HttpSecurity(that struct {
rememberme RememberMeService `@:"Inject"`
}) *HttpSecurity {
return (&HttpSecurityBuilder{}).
AuthorizedHTTPRequests().
Paths("/profile").Authenticated().
Paths("/todolists**").HasAuthority("ROLE_USER").
Paths("/admin", "/admin/**").HasAuthority("ROLE_ADMIN").
Paths("/static/**", "/*").PermitAll().
And().
AnyRequest().DenyAll().
FormLogin().ViewName("views/login").
And().
RememberMe(that.rememberme).
Build()
}Controller endpoints receive a typed struct with RequestMapping tags and the Fiber context:
type AdminController struct {
FiberController
RequestMapping `Path:"/admin"`
AppSecure
userService UserService `@:"Inject"`
}
func (this *AdminController) AdminListUsers(
_ struct { RequestMapping `Path:"/users"` },
ctx fiber.Ctx,
) error {
page := fiber.Query(ctx, "page", 1)
query := ctx.Query("q", "")
users := this.userService.FindAllUsersByUsername("%" + query + "%", page, 10)
return ctx.Render("views/fragments/admin-user-list", fiber.Map{
"Users": users,
"CurrentPage": page,
})
}This sample project is distributed under the terms of the project's repository license. Refer to the repository root for details.