In our previous post, we explored how to build a PDF invoice generator service with Encore and the Maroto package in Go. The focus was on how Encore simplifies backend development by taking care of the infrastructure management and API setup. This part two of the series builds on that foundation, showcasing how Encore handles authentication and database management, both essential components for modern cloud-native applications.

With Encore, we not only develop faster but also build highly scalable, secure applications, allowing us to focus on core functionality instead of getting bogged down by repetitive infrastructure tasks. By the end of this post, we’ll learn how these features help us to make our invoice generator service a product-ready app by ensuring security, support data interactions, and provide seamless testing of new code in production-like environments.

Why Authentication Matters?

Authentication is a fundamental requirement for almost every web application today. Whether you’re managing users for a SaaS product or securing access to an API, properly implementing authentication ensures that our data and services remain protected. Encore takes the hassle out of this process by offering native support for authentication, and easy integration with third-party providers like Auth0, Clerk and Firebase.

Setting Up Authentication in Encore

In our previous implementation, we had set a GenerateInvoice() method as a public endpoint, making it accessible to everyone (we can review the code again here). However, that’s not how endpoints usually work; Typically our applications are secured using some form of authentication. That’s why we want to make a small change in our endpoint to add a security layer. Encore simplifies handling authentication in backend services. It offers three types of API access:

  • Public: Accessible by anyone.
  • Private: Only accessible from other services in our app.
  • Auth: Requires user authentication before access is granted.

When defining an API with the auth annotation, Encore ensures that each API request contains valid authentication data, typically passed via the Authorization: Bearer <token> header. This is verified through a designated authentication handler, which we can customize to validate the token and retrieve the user’s ID or other relevant information​. Here’s how to implement it:

  1. Updating our GenerateInvoice endpoint definition
    Given that our GenerateInvoice endpoint is already defined, the only change we need to make is to update the annotation signature by changing public for auth.

    //encore:api auth method=POST path=/generate-invoice
    func GenerateInvoice(ctx context.Context, data *model.Request) (model.Response, error) {
    	response, err := invoice.Generate(data)
    	if err != nil {
    		return model.Response{}, err
    	}
    
    	return response, nil
    }
    

    This change will keep our GenerateInvoice method as a public endpoint that anybody can call, but that requires authentication. Now, if we try to run our app using $ encore run, we will get an error saying: No Auth Handler Defined, that’s expected, we have added an authentication layer, but nobody is handling it. To solve that, let’s create an AuthHandler.

  2. Defining a Authentication handler
    Encore allows us to create an authentication handler with the name we prefer, however, it must be exported and should contain the //encore:authhandler annotation, such as:

    //encore:authhandler
    func AuthHandler(ctx context.Context, token string) (auth.UID, error) {
    	if strings.EqualFold(token, "our-auth-token-here") {
    		return "accepted", nil
    	}
    
    	return "", &errs.Error{
    		Code:    errs.Unauthenticated,
    		Message: "invalid token",
    	}
    }
    

    Now that we have the AuthHandler in place, let’s test it.

  3. Testing It
    There are multiple ways to test our endpoint, one of them is by using the Encore Development Dashboard, which will automatically display an input to add the authentication token and send it as a request header.

    encore development dashboard

    Another way to test it is by using any REST API client like Postman, or via curl in our terminal:

    $ curl -X POST -H "Authorization: Bearer our-auth-token-here" \
    -H "Content-Type: application/json" \
    -d '{"CustomerName": "Jane Doe", "InvoiceNumber": "12345"}' \
    http://localhost:4000/generate-invoice
    

    And there you have it… or so it seems. Well, not quite! 😬

If you caught it, we are exposing our API token in the code, which is not a good practice. As developers, we usually store sensitive information like tokens in environment variables. Encore provides a feature called Secrets, which helps keep things secure. Let’s quickly update the code to hide our token properly.

Creating a Secret

Creating secrets in Encore is as simple as defining a Go struct with all the sensitive information we want to secure in our app:

var secrets struct {
	APIToken       string
   AnyOtherSecret string
   // ...
}

Note: All the struct attributes must be of type string.

Now, we need to update our validation. Instead of using a hardcoded token, we can use the secret value. Encore’s compiler ensures that all secrets are set before running or deploying our application. If any secret is missing, we’ll get an error while compiling. Once the secrets are defined, we can use them just like regular variables. Let’s update our condition:

// Before ❌
if strings.EqualFold(token, "our-auth-token-here") {
   return "accepted", nil
}

// Now ✅
if strings.EqualFold(token, secrets.APIToken) {
   return "accepted", nil
}

To set the value of our APIToken secret in the local environment, we can use Encore’s CLI by running the following command

$ encore secret set --type local APIToken

We’ll then be prompted to enter the secret value for APIToken.

By securing API endpoints using secrets, we avoid to expose sensitive value in our code but also ensure that only authenticated users have access to our services, such as our invoice generator.

Storing Invoices in a PostgreSQL Database

When building any program related with invoices, one important aspect is being able to retrieve each created invoice at any point in time for reference or audit purposes. In order to implement this, we will enhance our existing service by making sure that all the invoices created are kept in a database, along with an unique Invoice ID. Also, we will create a new service which will be used to obtain any previously generated invoice. This way, not only will the system be capable of generating invoices, but also capable of retrieval as necessary, as the system scales.

By using Encore’s integrated database system, we simplify the entire process of managing and accessing stored invoices while keeping the system scalable and efficient. Encore handles SQL databases as logical resources and natively supports PostgreSQL databases. Let’s start by setting up our database and migrations:

// Creates the invoice_generator database and assign it to the "db" variable
var db = sqldb.NewDatabase("invoice_generator", sqldb.DatabaseConfig{
	Migrations: "./migrations",
})

Here we are creating our invoice_generator database, and setting up our migrations folder location. Now let’s create an invoices table through a database migration.

-- api/migrations/1_create_invoices.up.sql
CREATE TABLE invoices (
   id BIGSERIAL PRIMARY KEY,
   data BYTEA NOT NULL
);

Fantastic, now we have created an invoices’ table, and what we need to do now is to create an insert method to store an invoice, and a find method to look for a single invoice.

func createInvoice(ctx context.Context, data []byte) (int, error) {
	var id int
	err := db.QueryRow(ctx, `
		INSERT INTO invoices (data)
		VALUES ($1)
		RETURNING id
	`, data).Scan(&id)

	return id, err
}

func findInvoice(ctx context.Context, id int) (model.Invoice, error) {
	invoice := model.Invoice{}

	err := db.QueryRow(ctx, `
	   SELECT id, data
	   FROM invoices
	   WHERE id = $1
	`, id).Scan(&invoice.ID, &invoice.Data)

	return invoice, err
}

The next step is to use our insert method by updating the GenerateInvoice service we have in api/invoice.go to store the invoice after it is generated:

//encore:api auth method=POST path=/generate-invoice
func GenerateInvoice(ctx context.Context, data *model.Request) (model.Response, error) {
	response, err := invoice.Generate(data)
	if err != nil {
		return model.Response{}, err
	}

	// Storing the invoice
	id, err := createInvoice(ctx, response.Data)
	if err != nil {
		errorResponse := &errs.Error{
			Code:    errs.Internal,
			Message: err.Error(),
		}

		return model.Response{}, errorResponse
	}

	response.ID = id

	return response, nil
}

Now every invoice we generate will be stored in our service’s database. Additionally, we updated the Response struct to include an invoice ID, so now it looks like this:

type Response struct {
	ID   int
	Data []byte
}

Introducing the GetInvoice Service

Once invoices are saved in the database, the next step is to provide a way for users to get them using the invoice’s unique ID that was generated and provided when the invoice was created. To implement this, we will create a new Encore’s service. This service will allow users to fetch the PDF data of a specific invoice directly from the database, ensuring that the invoices are always accessible for review, audits, or sharing. Our GetInvoice service is defined as follows:

//encore:api auth method=GET path=/get-invoice/:id
func GetInvoice(ctx context.Context, id int) (model.Response, error) {
	invoice, err := findInvoice(ctx, id)

	if err != nil {
		apiResponse := model.Response{
			Status:  http.StatusInternalServerError,
			Message: err.Error(),
		}

		if errors.Is(err, sql.ErrNoRows) {
			apiResponse.Status = http.StatusNotFound
			apiResponse.Message = "invoice not found"
		}

		return apiResponse, err
	}

	response := model.Response{
		Status:  http.StatusOK,
		Invoice: &invoice,
	}

	return response, nil
}

This GetInvoice service provides an API endpoint that returns the byte data of the stored invoice. Encore facilitates a way to set URL parameters in the annotation, such an id, with the form /get-invoice/:id, and access to the value as an argument. Additionally, we have created this endpoint with type auth, which means that invoices are only accessed by authenticated clients. Lastly, we have added a validation to check that an invalid ID is handled correctly by returning appropriate error messages when an invoice is not found.

To test this, we can go to our Encore Development Dashboard and perform a few calls.

Creating an invoice

First, select the api.GenerateInvoice endpoint in the Encore Development Dashboard. This allows us to generate a new invoice by filling out the necessary details and submitting the request.

create invoice

This request will give us not only the PDF data but also the associated ID we can use later to get that single invoice.

Getting an invoice

Once an invoice is created and we get the invoice ID, we can retrieve the PDF invoice at any time by calling to /get-invoice/[INVOICE_ID]. Let’s test it by selecting api.GetInvoice in our Encore Development Dashboard and passing the desired ID.

find invoice

And that’s it! 🎉 We’ve successfully implemented and tested the creation and retrieval of invoices using the Encore. With authentication and database management now integrated, our invoice generator service is more robust and ready for real-world use.

We have updated a bit our Response struct and added a creation date to the invoice. To explore the full code implementation of this invoice generator service, including the integration of authentication and database management with Encore, the source code is available in our GitHub repository.

Closing Thoughts

Integrating Encore’s authentication and database features into our invoice generator service represents a huge improvement since now we have a more secure and useful service. By using Encore, we have simplified a lot of the backend complexity, allowing us to focus on developing features without the work of managing infrastructure. This process not only enhances security through a powerful authentication mechanisms but also ensures that our database interactions are simple and efficient.

Moving on, the foundation we’ve built enables us to scale up our application and add new functionalities without being afraid of performance overhauls. Whether we’re working on invoice generation or any other service, Encore’s framework proves to be one powerful tool that facilitates modern web application development by requiring very minimal effort in managing infrastructures. The integration ensures a solid step toward making the system reliable and scalable, thus setting us up for future development.

References

  1. What is a Core functionality.
  2. What is SaaS.
  3. Exported Names in Go.
  4. What is a Request Header.
  5. An Introduction to Environment Variables and How to Use Them.
  6. Authenticating Users with Encore.
  7. Encore’s SQL Databases.