⚡️Blixt
[!CAUTION]
Blixt is in early development, and is not yet fully functional.
Additionally, the stdlib blocks are not yet all implemented.
Blixt is Firebase's evil relative, seen once a year at family reunions.
Write a config file, and Blixt will generate an extensible Go backend for you.
Use library blocks to add functionality, and deploy as a monolith or microservices with granular control.
Quickstart
Install Blixt:
go install github.com/Xavier-Maruff/blixt@latest
Create a new Blixt project, and follow the prompts:
blixt new myproject
Blocks
Blixt backends consist of blocks, which are logical units of functionality.
Each block is a separate module that is declared in the config file, and connects to other blocks to provide rich functionality.
Some common blocks are:
- auth: User authentication and authorization
- user: User management
- file: File storage
- db: SQLite database access
- notify: Email notifications
Under the hood, each block is a separate ConnectRPC service, which makes it easy to segment groups of blocks over the network.
Examples
Blixt is configured through a Pkl file blixt.pkl, at the root of your project.
In this file, you can import third-party blocks, define your own blocks, and set up connections between them.
Here is an example of a simple Blixt configuration that uses the standard lib to provide user management and authentication:
amends "https://lib.blxt.org/pkl/base.pkl"
import "https://lib.blxt.org/pkl/stdlib.pkl"
blocks = new {
stdlib.auth
stdlib.user
}
The standard library blocks automatically connect to each other, so in this case we don't need
to define any connections between them.
This will generate a monolithic backend, which we can access like a regular REST API with JSON, or via gRPC.
If we want to instead deploy it as two separate services, we can
define 'groups':
// ... same as before
groups = new {
(blixt.group) {
name = "a_group"
blocks = new {
// the block.name field uniquely identifies a block
stdlib.auth.name
}
}
(blixt.group) {
name = "b_group"
blocks = new {
stdlib.user.name
}
}
}
Every group produces an independent main package, which can be run as a separate service. Block method
calls are automatically forwarded between groups via gRPC, so your code doesn't change at all,
no matter how you define your groups.
We can extend the functionality by adding more blocks, or by creating our own.
The 'stdlib.user' block, for example, connects optionally to the 'stdlib.notify'
block to verify user emails. If we want to instead implement our own
notification system, we can define a new block, and connect it to the user block
in the slot where the 'stdlib.notify' block would go:
amends "https://lib.blxt.org/pkl/base.pkl"
import "https://lib.blxt.org/pkl/stdlib.pkl"
blocks = new {
notify
stdlib.auth
stdlib.user {
connections = (stdlib.user.connections) {
["notify"] = notify.name
}
config = (stdlib.user.config) {
["verify_users"] = true
}
}
}
//we define our own custom block like this
notify = (blixt.block) {
name = "notify"
//the go package where the block's methods are implemented
package = "this/go/package"
remote = false
//we implement the same interface as the stdlib.notify block
methods = stdlib.notify.methods
}
We could then implement the block in the package this/go/package, like this:
package notify
import (
"context"
"log"
apiv1 "path/to/blixt/gen/proto/v1"
"path/to/blixt/gen/notify"
)
type impl struct {
//store any persistent state here
}
func New() (notify.Block, error) {
//do any setup here
return &impl{}, nil
}
func (s *state) Connect(ctx context.Context, block string, conn interface{}) error {
//this is called when we register a connection from this block to another
//in this case, we don't need to do anything
}
//now we implement the methods defined in the block
//the request/response types are named {block name}{method name}Request/Response
func (s *state) Send(ctx context.Context, req *apiv1.NotifySendRequest)
(*apiv1.NotifySendResponse, error) {
//send the email
log.Printf("Sending email to %s with subject %s and message %s", req.Address, req.Subject, req.Message)
return &apiv1.NotifySendResponse{Success: true}, nil
}