Understanding github.com/dracory/userstore
The userstore package is Dracory's reusable user persistence layer. It wraps SQL databases behind a fluent, interface-driven API so your apps can manage users without bespoke SQL.
Architecture at a Glance
| Component | Purpose |
|---|---|
store implementation |
Maintains DB handle and exposes the high-level CRUD/sync methods defined by StoreInterface. Schema is created via migration files. |
user entity |
Dataobject-backed struct with fluent getters/setters, password helpers, and convenience methods like IsActive() and ProfileImageOrDefaultUrl(). |
role / userRole entities |
Optional role support (separate role and user_role tables) enabled via RolesEnabled. Built-in role handles: USER_ROLE_SUPERUSER, USER_ROLE_ADMINISTRATOR, USER_ROLE_MANAGER, USER_ROLE_USER. |
group / userGroup entities |
Optional group support (separate group and user_group tables) enabled via GroupsEnabled. |
UserQueryInterface |
Fluent builder for filtering, sorting, paginating, and counting users; validates inputs before issuing SQL. |
| Dependencies | neat for the SQL DSL/schema/sort/ORM contracts, carbon for timestamps, str for hashing, uid for IDs. |
Initialising a Store
store, err := userstore.NewStore(userstore.NewStoreOptions{
DB: db, // *sql.DB connection
UserTableName: "snv_users_user", // table name to target
RolesEnabled: true, // enable separate role tables
RoleTableName: "snv_users_role",
UserRoleTableName: "snv_users_user_role",
})
if err != nil {
log.Fatal(err)
}
- The database driver is auto-detected from the
*sql.DBconnection vianeat.NewFromSQLDB; there is noDbDriverNameoption in current versions. RolesEnabledrequires bothRoleTableNameandUserRoleTableName.GroupsEnabledrequires bothGroupTableNameandUserGroupTableName.-
Schema creation is handled by migration files in
database/migrations/(invoked viamigrations.MigrateAll(app)), not by automigration. The store does not setAutomigrateEnabled: true.
Creating and Updating Users
ctx := context.Background()
user := userstore.NewUser().
SetEmail("ada@example.com").
SetFirstName("Ada").
SetLastName("Lovelace").
SetStatus(userstore.USER_STATUS_ACTIVE)
if err := store.UserCreate(ctx, user); err != nil {
return err
}
// Update selective fields via fluent setters
user.SetBusinessName("Babbage Analytics")
if err := store.UserUpdate(ctx, user); err != nil {
return err
}
Key helpers:
SetPasswordAndHashhashes via bcrypt before storing.-
SoftDeletedAt()/UserSoftDeletemark rows with a timestamp instead of removing them; normal list queries hide soft-deleted users by default. Metas,SetMeta,UpsertMetasmanage arbitrary metadata as JSON.
Querying with Precision
query := userstore.NewUserQuery().
SetStatus(userstore.USER_STATUS_ACTIVE).
SetCreatedAtGte("2025-01-01 00:00:00").
SetOrderBy("created_at").
SetSortDirection("desc").
SetLimit(20)
users, err := store.UserList(ctx, query)
count, err := store.UserCount(ctx, query)
userSelectQuery composes the SQL via neat ORM contracts and applies soft-delete filters
unless SoftDeletedIncluded() is set. Column lists are configurable with
SetColumns() so callers can project only the data they need.
Roles
Roles live in their own tables (since Blueprint v0.45.0), not on the user row.
A role is identified by a handle; assignments are user_role join rows.
// Find (or lazily create) a role by handle
role, err := store.RoleFindByHandleOrCreate(ctx,
userstore.USER_ROLE_ADMINISTRATOR,
userstore.USER_STATUS_ACTIVE,
)
// Assign it to a user
_, err = store.UserRoleFindByUserIDAndRoleIDOrCreate(ctx, user.GetID(), role.GetID())
// Check membership
ok, err := store.UserHasRoles(ctx, user.GetID(), []string{role.GetID()})
// All roles for a user
roles, err := store.UserRoles(ctx, user.GetID())
-
Role/user-role queries mirror the user API:
RoleList,RoleCount,RoleSoftDelete,UserRoleList,UserRoleCount,UserRoleSoftDelete, etc. -
In the app layer, prefer
helpers.UserHasActiveRole(ctx, app, authUser, userstore.USER_ROLE_ADMINISTRATOR)over checking a flag on the user entity. - New registrations automatically receive the
USER_ROLE_USERrole. -
Groups work the same way when
GroupsEnabledis set (GroupList,GroupFindByHandle,UserGroupCreate,UserGroups, ...).
Related Reading
- Design Principles – understand the interface-first approach underlying userstore.
- Entities & Interfaces – deep dive into dataobject-backed models.
- Testing – leverage the package's SQLite support for fast, isolated tests.
- Create Your Own Store – adapt userstore patterns for your own domain.