Jelly
Jelly is a modular Go backend framework. The root module, github.com/waffleplus/jelly, is the core runtime. Web, database, auth, mail, and other features live in optional submodules inside the same repository.
The design goal is simple:
- small core
- explicit module registration
- constructor-based wiring
- centralized config loading
- optional features with minimal public APIs
Modules
github.com/waffleplus/jelly - core runtime, lifecycle, config, logging, container
github.com/waffleplus/jelly/web - Gin-based HTTP server, binding, responses, middleware contracts
github.com/waffleplus/jelly/health - health contracts and service
github.com/waffleplus/jelly/db - DB runtime, provider registration, sessions, transactions
github.com/waffleplus/jelly/postgres - Postgres provider and migrations
github.com/waffleplus/jelly/sqlx - optional sqlx adapter on top of jelly/db
github.com/waffleplus/jelly/jdb - generic CRUD helpers on top of jelly/db
github.com/waffleplus/jelly/auth - JWT, OIDC, and passkeys
github.com/waffleplus/jelly/mail - mail runtime and SMTP sender
Recommended import aliases:
import (
"github.com/waffleplus/jelly"
jauth "github.com/waffleplus/jelly/auth"
jdb "github.com/waffleplus/jelly/db"
jmail "github.com/waffleplus/jelly/mail"
jpostgres "github.com/waffleplus/jelly/postgres"
jsqlx "github.com/waffleplus/jelly/sqlx"
jweb "github.com/waffleplus/jelly/web"
)
Quick Start
package main
import (
"github.com/waffleplus/jelly"
jauth "github.com/waffleplus/jelly/auth"
jdb "github.com/waffleplus/jelly/db"
jmail "github.com/waffleplus/jelly/mail"
jpostgres "github.com/waffleplus/jelly/postgres"
jsqlx "github.com/waffleplus/jelly/sqlx"
jweb "github.com/waffleplus/jelly/web"
)
func main() {
app := jelly.New(
jweb.Module(),
jdb.Module(),
jpostgres.Module(),
jsqlx.Module(),
jauth.Module(),
jmail.Module(),
)
if err := app.Run(); err != nil {
panic(err)
}
}
Module registration is explicit. The core does not start web, DB, auth, or mail unless the app asks for them.
Wiring
Jelly uses constructor-based registration. For app-scoped wiring, use jelly.Register[T](app, ...). The legacy jelly.Bean[T](...) API still exists for the default global app and jelly.RunApplication(...).
package wire
import (
"github.com/waffleplus/jelly"
jweb "github.com/waffleplus/jelly/web"
"myapp/internal/ping/application"
"myapp/internal/ping/application/in"
"myapp/internal/ping/application/out"
"myapp/internal/ping/http"
"myapp/internal/ping/postgres"
)
func Register(app *jelly.App) {
jelly.Register[http.PingHandler](app, http.NewPingHandler, jelly.As[jweb.RouteProvider]())
jelly.Register[application.Service](app, application.NewService, jelly.As[in.PingUseCase]())
jelly.Register[postgres.PingRepository](app, postgres.NewPingRepository, jelly.As[out.PingRepository]())
}
Configuration
The core loads:
config/application.yaml
config/application-<env>.yaml when jelly.app.env or JELLY_ENV is set
- placeholders such as
${VAR} and ${VAR:default}
At startup, Jelly also loads .env automatically when the file exists in the working
directory or module root. Existing process environment variables keep priority over
file values.
Everything stays under the jelly: root. Each module binds only its own subtree.
jelly:
app:
name: ${APP_NAME:my-app}
env: ${JELLY_ENV:dev}
debug: ${JELLY_DEBUG:true}
log:
level: ${LOG_LEVEL:info}
format: ${LOG_FORMAT:pretty}
output: ${LOG_OUTPUT:stdout}
web:
port: ${SERVER_PORT:8080}
base-path: /api
db:
driver: postgres
postgres:
host: ${DATABASE_HOST:localhost}
port: ${DATABASE_PORT:5432}
name: ${DATABASE_NAME:app}
user: ${DATABASE_USER:postgres}
password: ${DATABASE_PASSWORD:postgres}
Presence Enables Defaults
Config blocks support presence-based activation:
jelly:
auth:
passkey:
jelly:
auth:
passkey: {}
jelly:
auth:
passkey: true
All three mean:
- the block is present
- the feature is enabled
- module defaults are applied
- explicit user fields override those defaults
false means absent.
Public Surface
Each module keeps a small public facade. Implementation and wiring details live under internal.
Examples:
jweb.Module()
jdb.Module()
jpostgres.Module()
jsqlx.Module()
jauth.Module()
jmail.Module()
jweb.RouteProvider
jdb.Provider
jauth.GenerateToken
jauth.VerifyGoogleIDToken
jauth.BuildRegistrationOptions
jauth.VerifyAuthentication
jmail.Send
Not part of the public contract:
- module config structs used only for YAML binding
- module default and validation helpers used only by runtime wiring
- internal service types behind top-level helpers
Dependency Direction
web -> jelly, health
db -> jelly
postgres -> db, health
sqlx -> db
jdb -> db
auth -> jelly
mail -> jelly
The core must not depend on concrete technologies like Gin, Postgres, SMTP, or WebAuthn.
Passkeys
Passkeys are part of github.com/waffleplus/jelly/auth, not a separate root package.
Configuration lives under jelly.auth.passkey.
The supported public API is the top-level helper set:
jauth.BuildRegistrationOptions
jauth.VerifyRegistration
jauth.BuildAuthenticationOptions
jauth.LookupAuthenticationCredential
jauth.VerifyAuthentication
Passkey credential payloads are typed:
jauth.RegistrationCredential
jauth.AuthenticationCredential
If an app starts from raw JSON instead of direct binding, it can use:
jauth.ParseRegistrationCredential
jauth.ParseAuthenticationCredential
Applications should treat ceremony state as opaque and keep persistence in their own storage layer.
Repo Layout
Jelly uses one repo with multiple Go modules and a shared go.work:
jelly/
go.mod
go.work
auth/
db/
health/
jdb/
mail/
postgres/
sqlx/
web/
Compatibility
jelly.New(...) is the preferred bootstrap API.
jelly.RunApplication(...) still works for the default app.
- Existing apps should migrate imports from root features to their owning modules:
passkey -> auth
- DB runtime ->
db
- Postgres runtime ->
postgres
- mail ->
mail
More