Skip to main content

Your first service

While 1Backend itself is written in Go, services that run on it can be written in any language.

A service only needs a few things to fully function:

  • Register a user account, just like a human user. For details, see the User Svc.
  • Register its instance in the registry so 1Backend knows where to route requests.

A Go example

The following Go service demonstrates these steps:

  • Registers itself as a user with the slug basic-svc
  • Registers or updates its URL (http://127.0.0.1:9111) in the Registry.

You may notice that the following code uses a "Go SDK"—this is simply a set of convenience functions built on top of the 1Backend API. 1Backend is language-agnostic and can be used with any language, even if no SDK is available in the repository.

The full code, including tests, is available in the examples directory.

// <!-- INCLUDE: ../../../examples/go/services/basic/internal/basic_service.go -->
package basicservice

import (
"context"
"net/http"

openapi "github.com/1backend/1backend/clients/go"
basic "github.com/1backend/1backend/examples/go/services/basic/internal/types"
sdk "github.com/1backend/1backend/sdk/go"
"github.com/1backend/1backend/sdk/go/auth"
"github.com/1backend/1backend/sdk/go/boot"
"github.com/1backend/1backend/sdk/go/client"
"github.com/1backend/1backend/sdk/go/datastore"
"github.com/1backend/1backend/sdk/go/infra"
"github.com/1backend/1backend/sdk/go/middlewares"
"github.com/gorilla/mux"
"github.com/pkg/errors"
)

const RolePetManager = "basic-svc:pet:manager"

type BasicService struct {
Options *boot.Options

token string
userSvcPublicKey string

dataStoreFactory infra.DataStoreFactory

petsStore datastore.DataStore
credentialStore datastore.DataStore

Router *mux.Router
}

type Options struct {
Test bool
ServerUrl string
SelfUrl string
}

func NewService(options *boot.Options) (*BasicService, error) {
options.LoadEnvars()

dconf := infra.DataStoreConfig{}
if options.Test {
dconf.Test = true
dconf.TablePrefix = sdk.Id("t")
}

service := &BasicService{
Options: options,
}

dsf, err := infra.NewDataStoreFactory(dconf)
if err != nil {
return nil, errors.Wrap(err, "cannot create datastore factory")
}
service.dataStoreFactory = dsf

petStore, err := dsf.Create("basicSvcPets", &basic.Pet{})
if err != nil {
return nil, err
}
service.petsStore = petStore

service.registerAccount()
service.registerRoutes()

return service, nil
}

func (service *BasicService) Start() error {
client := client.NewApiClientFactory(service.Options.ServerUrl).
Client(client.WithToken(service.token))

_, _, err := client.RegistrySvcAPI.
RegisterInstance(context.Background()).
Body(openapi.RegistrySvcRegisterInstanceRequest{
Url: service.Options.SelfUrl,
}).Execute()
if err != nil {
return errors.Wrap(err, "cannot register instance")
}

return nil
}

func (service *BasicService) registerAccount() error {
credentialStore, err := service.dataStoreFactory.Create("basicSvcCredentials", &auth.Credential{})
if err != nil {
return errors.Wrap(err, "cannot create credential store")
}
service.credentialStore = credentialStore

obClient := client.NewApiClientFactory(service.Options.ServerUrl).Client()
token, err := boot.RegisterServiceAccount(
obClient.UserSvcAPI,
"basic-svc",
"Basic Svc",
service.credentialStore,
)
if err != nil {
return errors.Wrap(err, "cannot register service")
}
service.token = token.Token

obClient = client.NewApiClientFactory(service.Options.ServerUrl).
Client(client.WithToken(service.token))

_, _, err = obClient.RegistrySvcAPI.
RegisterInstance(context.Background()).
Body(openapi.RegistrySvcRegisterInstanceRequest{
Url: service.Options.SelfUrl,
}).Execute()
if err != nil {
return errors.Wrap(err, "cannot register instance")
}

pk, _, err := obClient.
UserSvcAPI.GetPublicKey(context.Background()).
Execute()
if err != nil {
return err
}
service.userSvcPublicKey = pk.PublicKey

return nil
}

func (service *BasicService) registerRoutes() {
mws := []middlewares.Middleware{
middlewares.ThrottledLogger,
middlewares.Recover,
middlewares.CORS,
middlewares.GzipDecodeMiddleware,
}
appl := applicator(mws)

service.Router = mux.NewRouter()

service.Router.HandleFunc("/basic-svc/pet", appl(func(w http.ResponseWriter, r *http.Request) {
service.SavePet(w, r)
})).
Methods("OPTIONS", "PUT")

service.Router.HandleFunc("/basic-svc/pets", appl(func(w http.ResponseWriter, r *http.Request) {
service.ListPets(w, r)
})).
Methods("OPTIONS", "POST")
}

func applicator(
mws []middlewares.Middleware,
) func(http.HandlerFunc) http.HandlerFunc {
return func(h http.HandlerFunc) http.HandlerFunc {
for _, middleware := range mws {
h = middleware(h)
}

return h
}
}
// <!-- /INCLUDE -->

Make sure to run it with the appropriate environment variables:

OB_SERVER_URL=http://127.0.0.1:11337 OB_SELF_URL=http://127.0.0.1:9111 go run main.go

Once it's running, you'll be able to call the 1Backend server proxy, which will forward the request to your basic service:

# 127.0.0.1:11337 here is the address of the 1Backend server
$ curl 127.0.0.1:11337/basic-svc/hello
{"hello": "world"}

This means you don't have to expose your basic service to the outside world—only the 1Backend server needs to be accessible.

Let's recap how the proxying works:

  • Service registers an account, acquires the basic-svc slug.
  • Service calls the 1Backend Registry Svc to tell the system an instance of the Basic service is available under the URL http://127.0.0.1:9111
  • When you send a request to the 1Backend server with a path like 127.0.0.1:11337/basic-svc/hello, the first section of the path is interpreted as a user account slug. The server checks what instances are owned by that slug and routes the request to one of those instances.
$ oo instance ls
ID URL STATUS OWNER SLUG LAST HEARTBEAT
inst_eHFTNvAlk9 http://127.0.0.1:9111 Healthy basic-svc 10s ago

Things to understand

Instance registration

Like most other things on the platform, service instances are owned by a user account slug. When the basic service calls RegisterInstance, the host will be associated with the basic-svc slug.

Updates to this host won’t be possible unless the caller is the basic service itself or an admin. In essence, the service becomes the owner of that URL.

This is the same ownership model used throughout the 1Backend system.