about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEmile <git@emile.space>2025-02-12 22:05:22 +0100
committerEmile <git@emile.space>2025-02-12 22:05:22 +0100
commit7152205c80f059d3649e1830fb4dfc46d1fc158f (patch)
treeb0d9a9aea760fabd61ee5cb06814d45cac5bb629
parent4d08790c43b2d0720ef43b657a651a7c541d30d2 (diff)
template: the goapp docker package should now (in theory) build
It's quite a weird way to pull out the packge name from the attribute
set of defined packages, yet it kind of works.

I can't test it, as docker doesn't want to run Mach-O binaries, but
kicking this into hydra should result in some nice builds.
-rw-r--r--nix/templates/goapp/backend/db.go37
-rw-r--r--nix/templates/goapp/backend/default.nix15
-rw-r--r--nix/templates/goapp/backend/go.mod22
-rw-r--r--nix/templates/goapp/backend/go.sum57
-rw-r--r--nix/templates/goapp/backend/log.go34
-rw-r--r--nix/templates/goapp/backend/main.go80
-rw-r--r--nix/templates/goapp/backend/sqlitestore.go284
-rw-r--r--nix/templates/goapp/backend/src/main.go29
-rw-r--r--nix/templates/goapp/flake.nix21
-rw-r--r--nix/templates/goapp/frontend/db.go37
-rw-r--r--nix/templates/goapp/frontend/default.nix15
-rw-r--r--nix/templates/goapp/frontend/go.mod24
-rw-r--r--nix/templates/goapp/frontend/go.sum57
-rw-r--r--nix/templates/goapp/frontend/log.go34
-rw-r--r--nix/templates/goapp/frontend/main.go80
-rw-r--r--nix/templates/goapp/frontend/sqlitestore.go284
-rw-r--r--nix/templates/goapp/frontend/src/main.go29
17 files changed, 1050 insertions, 89 deletions
diff --git a/nix/templates/goapp/backend/db.go b/nix/templates/goapp/backend/db.go
new file mode 100644
index 0000000..fd3605a
--- /dev/null
+++ b/nix/templates/goapp/backend/db.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+	"database/sql"
+	"log"
+
+	_ "github.com/mattn/go-sqlite3"
+)
+
+const create string = `
+CREATE TABLE IF NOT EXISTS users (
+	id INTEGER NOT NULL PRIMARY KEY,
+	created_at DATETIME NOT NULL,
+	name TEXT,
+	passwordHash TEXT
+);
+`
+
+type State struct {
+	db       *sql.DB      // the database storing the "business data"
+	sessions *SqliteStore // the database storing sessions
+}
+
+func NewState() (*State, error) {
+	db, err := sql.Open("sqlite3", databasePath)
+	if err != nil {
+		log.Println("Error opening the db: ", err)
+		return nil, err
+	}
+	if _, err := db.Exec(create); err != nil {
+		log.Println("Error creating the tables: ", err)
+		return nil, err
+	}
+	return &State{
+		db: db,
+	}, nil
+}
diff --git a/nix/templates/goapp/backend/default.nix b/nix/templates/goapp/backend/default.nix
index 1a3aeeb..f4ed3a4 100644
--- a/nix/templates/goapp/backend/default.nix
+++ b/nix/templates/goapp/backend/default.nix
@@ -1,21 +1,16 @@
-{ pkgs, packagename, ... }:
+{ pkgs, name, ... }:
 
 let
   version = "0.0.1";
 in
 pkgs.buildGoModule {
-  name = "${packagename}-${version}";
-  pname = "${packagename}";
+  name = "${name}-${version}";
+  pname = "${name}";
   version = "${version}";
 
   src = ./.;
-  subPackages = [ "src" ];
-  vendorHash = "sha256-8wYERVt3PIsKkarkwPu8Zy/Sdx43P6g2lz2xRfvTZ2E=";
-
-  postInstall = ''
-    mkdir -p $out
-    mv $out/bin/src $out/bin/${packagename}
-  '';
+  subPackages = [ "" ];
+  vendorHash = "sha256-tIk8lmyuVETrOW7fA7K7uNNXAAtJAYSM4uH+xZaMWqc=";
 
   doCheck = true;
 }
diff --git a/nix/templates/goapp/backend/go.mod b/nix/templates/goapp/backend/go.mod
index 41401c9..40a7d7f 100644
--- a/nix/templates/goapp/backend/go.mod
+++ b/nix/templates/goapp/backend/go.mod
@@ -2,4 +2,24 @@ module github.com/hanemile/goapp/backend
 
 go 1.23.5
 
-require github.com/gorilla/mux v1.8.1
+require (
+	github.com/gorilla/handlers v1.5.2
+	github.com/gorilla/mux v1.8.1
+	github.com/gorilla/securecookie v1.1.2
+	github.com/gorilla/sessions v1.4.0
+	modernc.org/sqlite v1.34.5
+)
+
+require (
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/felixge/httpsnoop v1.0.4 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mattn/go-sqlite3 v1.14.24 // indirect
+	github.com/ncruces/go-strftime v0.1.9 // indirect
+	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+	golang.org/x/sys v0.22.0 // indirect
+	modernc.org/libc v1.55.3 // indirect
+	modernc.org/mathutil v1.6.0 // indirect
+	modernc.org/memory v1.8.0 // indirect
+)
diff --git a/nix/templates/goapp/backend/go.sum b/nix/templates/goapp/backend/go.sum
index 7128337..53d3f31 100644
--- a/nix/templates/goapp/backend/go.sum
+++ b/nix/templates/goapp/backend/go.sum
@@ -1,2 +1,59 @@
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
+github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
+github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
+github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
 github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
 github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
+github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
+github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
+github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
+golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
+golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
+modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
+modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
+modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
+modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
+modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
+modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
+modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
+modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
+modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
+modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
+modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
+modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
+modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
+modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
+modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
+modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/nix/templates/goapp/backend/log.go b/nix/templates/goapp/backend/log.go
new file mode 100644
index 0000000..5af719a
--- /dev/null
+++ b/nix/templates/goapp/backend/log.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+	"net/http"
+	"os"
+
+	"github.com/gorilla/handlers"
+)
+
+// Defines a middleware containing a logfile
+//
+// This is done to combine gorilla/handlers with gorilla/mux middlewares to
+// just use r.Use(logger.Middleware) once instead of adding this to all
+// handlers manually (Yes, I'm really missing macros in Go...)
+type loggingMiddleware struct {
+	logFile *os.File
+}
+
+func (l *loggingMiddleware) Middleware(next http.Handler) http.Handler {
+	return handlers.LoggingHandler(l.logFile, next)
+}
+
+func authMiddleware(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		session, _ := globalState.sessions.Get(r, "session")
+		username := session.Values["username"]
+
+		if username == nil {
+			http.Redirect(w, r, "/login", http.StatusSeeOther)
+		} else {
+			next.ServeHTTP(w, r)
+		}
+	})
+}
diff --git a/nix/templates/goapp/backend/main.go b/nix/templates/goapp/backend/main.go
new file mode 100644
index 0000000..d793869
--- /dev/null
+++ b/nix/templates/goapp/backend/main.go
@@ -0,0 +1,80 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"time"
+
+	"github.com/gorilla/mux"
+)
+
+var (
+	host          string
+	port          int
+	logFilePath   string
+	databasePath  string
+	sessiondbPath string
+	globalState   *State
+)
+
+func initFlags() {
+	flag.StringVar(&host, "host", "127.0.0.1", "The host to listen on")
+	flag.StringVar(&host, "h", "127.0.0.1", "The host to listen on (shorthand)")
+
+	flag.IntVar(&port, "port", 8080, "The port to listen on")
+	flag.IntVar(&port, "p", 8080, "The port to listen on (shorthand)")
+
+	flag.StringVar(&logFilePath, "logfilepath", "./server.log", "The path to the log file")
+	flag.StringVar(&databasePath, "databasepath", "./main.db", "The path to the main database")
+	flag.StringVar(&sessiondbPath, "sessiondbpath", "./sessions.db", "The path to the session database")
+}
+
+func indexHandler(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprintf(w, "Hello World from the backend")
+}
+
+func main() {
+	initFlags()
+	flag.Parse()
+
+	// log init
+	log.Println("[i] Setting up logging...")
+	logFile, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0664)
+	if err != nil {
+		log.Fatal("Error opening the server.log file: ", err)
+	}
+	logger := loggingMiddleware{logFile}
+
+	// db init
+	log.Println("[i] Setting up Global State Struct...")
+	s, err := NewState()
+	if err != nil {
+		log.Fatal("Error creating the NewState(): ", err)
+	}
+	globalState = s
+
+	// session init
+	log.Println("[i] Setting up Session Storage...")
+	store, err := NewSqliteStore(sessiondbPath, "sessions", "/", 3600, []byte(os.Getenv("SESSION_KEY")))
+	if err != nil {
+		panic(err)
+	}
+	globalState.sessions = store
+
+	r := mux.NewRouter()
+	r.Use(logger.Middleware)
+	r.HandleFunc("/", indexHandler)
+
+	srv := &http.Server{
+		Handler:      r,
+		Addr:         ":8080",
+		WriteTimeout: 15 * time.Second,
+		ReadTimeout:  15 * time.Second,
+	}
+
+	log.Printf("[i] Running the server on %s", srv.Addr)
+	log.Fatal(srv.ListenAndServe())
+}
diff --git a/nix/templates/goapp/backend/sqlitestore.go b/nix/templates/goapp/backend/sqlitestore.go
new file mode 100644
index 0000000..6f59d15
--- /dev/null
+++ b/nix/templates/goapp/backend/sqlitestore.go
@@ -0,0 +1,284 @@
+/*
+	Gorilla Sessions backend for SQLite.
+
+Copyright (c) 2013 Contributors. See the list of contributors in the CONTRIBUTORS file for details.
+
+This software is licensed under a MIT style license available in the LICENSE file.
+*/
+package main
+
+import (
+	"database/sql"
+	"encoding/gob"
+	"errors"
+	"fmt"
+	"log"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/gorilla/securecookie"
+	"github.com/gorilla/sessions"
+	_ "modernc.org/sqlite"
+)
+
+type SqliteStore struct {
+	db         DB
+	stmtInsert *sql.Stmt
+	stmtDelete *sql.Stmt
+	stmtUpdate *sql.Stmt
+	stmtSelect *sql.Stmt
+
+	Codecs  []securecookie.Codec
+	Options *sessions.Options
+	table   string
+}
+
+type sessionRow struct {
+	id         string
+	data       string
+	createdOn  time.Time
+	modifiedOn time.Time
+	expiresOn  time.Time
+}
+
+type DB interface {
+	Exec(query string, args ...interface{}) (sql.Result, error)
+	Prepare(query string) (*sql.Stmt, error)
+	Close() error
+}
+
+func init() {
+	gob.Register(time.Time{})
+}
+
+func NewSqliteStore(endpoint string, tableName string, path string, maxAge int, keyPairs ...[]byte) (*SqliteStore, error) {
+	db, err := sql.Open("sqlite3", endpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewSqliteStoreFromConnection(db, tableName, path, maxAge, keyPairs...)
+}
+
+func NewSqliteStoreFromConnection(db DB, tableName string, path string, maxAge int, keyPairs ...[]byte) (*SqliteStore, error) {
+	// Make sure table name is enclosed.
+	tableName = "`" + strings.Trim(tableName, "`") + "`"
+
+	cTableQ := "CREATE TABLE IF NOT EXISTS " +
+		tableName + " (id INTEGER PRIMARY KEY, " +
+		"session_data LONGBLOB, " +
+		"created_on TIMESTAMP DEFAULT 0, " +
+		"modified_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
+		"expires_on TIMESTAMP DEFAULT 0);"
+	if _, err := db.Exec(cTableQ); err != nil {
+		return nil, err
+	}
+
+	insQ := "INSERT INTO " + tableName +
+		"(id, session_data, created_on, modified_on, expires_on) VALUES (NULL, ?, ?, ?, ?)"
+	stmtInsert, stmtErr := db.Prepare(insQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	delQ := "DELETE FROM " + tableName + " WHERE id = ?"
+	stmtDelete, stmtErr := db.Prepare(delQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	updQ := "UPDATE " + tableName + " SET session_data = ?, created_on = ?, expires_on = ? " +
+		"WHERE id = ?"
+	stmtUpdate, stmtErr := db.Prepare(updQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	selQ := "SELECT id, session_data, created_on, modified_on, expires_on from " +
+		tableName + " WHERE id = ?"
+	stmtSelect, stmtErr := db.Prepare(selQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	return &SqliteStore{
+		db:         db,
+		stmtInsert: stmtInsert,
+		stmtDelete: stmtDelete,
+		stmtUpdate: stmtUpdate,
+		stmtSelect: stmtSelect,
+		Codecs:     securecookie.CodecsFromPairs(keyPairs...),
+		Options: &sessions.Options{
+			Path:   path,
+			MaxAge: maxAge,
+		},
+		table: tableName,
+	}, nil
+}
+
+func (m *SqliteStore) Close() {
+	m.stmtSelect.Close()
+	m.stmtUpdate.Close()
+	m.stmtDelete.Close()
+	m.stmtInsert.Close()
+	m.db.Close()
+}
+
+func (m *SqliteStore) Get(r *http.Request, name string) (*sessions.Session, error) {
+	return sessions.GetRegistry(r).Get(m, name)
+}
+
+func (m *SqliteStore) New(r *http.Request, name string) (*sessions.Session, error) {
+	session := sessions.NewSession(m, name)
+	session.Options = &sessions.Options{
+		Path:   m.Options.Path,
+		MaxAge: m.Options.MaxAge,
+	}
+	session.IsNew = true
+	var err error
+	if cook, errCookie := r.Cookie(name); errCookie == nil {
+		err = securecookie.DecodeMulti(name, cook.Value, &session.ID, m.Codecs...)
+		if err == nil {
+			err = m.load(session)
+			if err == nil {
+				session.IsNew = false
+			} else {
+				err = nil
+			}
+		}
+	}
+	return session, err
+}
+
+func (m *SqliteStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
+	var err error
+	if session.ID == "" {
+		if err = m.insert(session); err != nil {
+			return err
+		}
+	} else if err = m.save(session); err != nil {
+		return err
+	}
+	encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, m.Codecs...)
+	if err != nil {
+		return err
+	}
+	http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options))
+	return nil
+}
+
+func (m *SqliteStore) insert(session *sessions.Session) error {
+	var createdOn time.Time
+	var modifiedOn time.Time
+	var expiresOn time.Time
+	crOn := session.Values["created_on"]
+	if crOn == nil {
+		createdOn = time.Now()
+	} else {
+		createdOn = crOn.(time.Time)
+	}
+	modifiedOn = createdOn
+	exOn := session.Values["expires_on"]
+	if exOn == nil {
+		expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
+	} else {
+		expiresOn = exOn.(time.Time)
+	}
+	delete(session.Values, "created_on")
+	delete(session.Values, "expires_on")
+	delete(session.Values, "modified_on")
+
+	encoded, encErr := securecookie.EncodeMulti(session.Name(), session.Values, m.Codecs...)
+	if encErr != nil {
+		return encErr
+	}
+	res, insErr := m.stmtInsert.Exec(encoded, createdOn, modifiedOn, expiresOn)
+	if insErr != nil {
+		return insErr
+	}
+	lastInserted, lInsErr := res.LastInsertId()
+	if lInsErr != nil {
+		return lInsErr
+	}
+	session.ID = fmt.Sprintf("%d", lastInserted)
+	return nil
+}
+
+func (m *SqliteStore) Delete(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
+
+	// Set cookie to expire.
+	options := *session.Options
+	options.MaxAge = -1
+	http.SetCookie(w, sessions.NewCookie(session.Name(), "", &options))
+	// Clear session values.
+	for k := range session.Values {
+		delete(session.Values, k)
+	}
+
+	_, delErr := m.stmtDelete.Exec(session.ID)
+	if delErr != nil {
+		return delErr
+	}
+	return nil
+}
+
+func (m *SqliteStore) save(session *sessions.Session) error {
+	if session.IsNew == true {
+		return m.insert(session)
+	}
+	var createdOn time.Time
+	var expiresOn time.Time
+	crOn := session.Values["created_on"]
+	if crOn == nil {
+		createdOn = time.Now()
+	} else {
+		createdOn = crOn.(time.Time)
+	}
+
+	exOn := session.Values["expires_on"]
+	if exOn == nil {
+		expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
+		log.Print("nil")
+	} else {
+		expiresOn = exOn.(time.Time)
+		if expiresOn.Sub(time.Now().Add(time.Second*time.Duration(session.Options.MaxAge))) < 0 {
+			expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
+		}
+	}
+
+	delete(session.Values, "created_on")
+	delete(session.Values, "expires_on")
+	delete(session.Values, "modified_on")
+	encoded, encErr := securecookie.EncodeMulti(session.Name(), session.Values, m.Codecs...)
+	if encErr != nil {
+		return encErr
+	}
+	_, updErr := m.stmtUpdate.Exec(encoded, createdOn, expiresOn, session.ID)
+	if updErr != nil {
+		return updErr
+	}
+	return nil
+}
+
+func (m *SqliteStore) load(session *sessions.Session) error {
+	row := m.stmtSelect.QueryRow(session.ID)
+	sess := sessionRow{}
+	scanErr := row.Scan(&sess.id, &sess.data, &sess.createdOn, &sess.modifiedOn, &sess.expiresOn)
+	if scanErr != nil {
+		return scanErr
+	}
+	if sess.expiresOn.Sub(time.Now()) < 0 {
+		log.Printf("Session expired on %s, but it is %s now.", sess.expiresOn, time.Now())
+		return errors.New("Session expired")
+	}
+	err := securecookie.DecodeMulti(session.Name(), sess.data, &session.Values, m.Codecs...)
+	if err != nil {
+		return err
+	}
+	session.Values["created_on"] = sess.createdOn
+	session.Values["modified_on"] = sess.modifiedOn
+	session.Values["expires_on"] = sess.expiresOn
+	return nil
+
+}
diff --git a/nix/templates/goapp/backend/src/main.go b/nix/templates/goapp/backend/src/main.go
deleted file mode 100644
index b9d9214..0000000
--- a/nix/templates/goapp/backend/src/main.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"log"
-	"net/http"
-	"time"
-
-	"github.com/gorilla/mux"
-)
-
-func indexHandler(w http.ResponseWriter, r *http.Request) {
-	fmt.Fprintf(w, "Hello World from the backend")
-}
-
-func main() {
-	r := mux.NewRouter()
-	r.HandleFunc("/", indexHandler)
-
-	srv := &http.Server{
-		Handler:      r,
-		Addr:         ":8080",
-		WriteTimeout: 15 * time.Second,
-		ReadTimeout:  15 * time.Second,
-	}
-
-	log.Printf("[i] Running the server on %s", srv.Addr)
-	log.Fatal(srv.ListenAndServe())
-}
diff --git a/nix/templates/goapp/flake.nix b/nix/templates/goapp/flake.nix
index 4e97240..132667d 100644
--- a/nix/templates/goapp/flake.nix
+++ b/nix/templates/goapp/flake.nix
@@ -3,7 +3,7 @@
   inputs.flake-utils.url = "git+https://github.com/numtide/flake-utils";
 
   outputs =
-    { nixpkgs, flake-utils, ... }:
+    { self, nixpkgs, flake-utils, ... }:
     flake-utils.lib.eachDefaultSystem (
       system:
       let
@@ -12,19 +12,24 @@
           overlays = [ ];
         };
 
-        package-and-docker = packagename: {
+        package-and-docker = name: system: (let
+          pkgname = name + "-pkg";
+          dockername = name + "-docker";
+        in {
           # the raw package
-          "${packagename}" = import ./${packagename} { inherit pkgs packagename; };
+          ${pkgname} = import ./${name} { inherit pkgs name; };
 
           # the docker image
-          "${packagename}-docker" = pkgs.dockerTools.buildImage {
-            name = "${packagename}";
-            config.Cmd = [ "${packagename}/bin/${packagename}" ];
+          ${dockername} = pkgs.dockerTools.buildImage {
+            name = "${name}";
+            config.Cmd = [ "${self.packages.${system}.${pkgname}}/bin/${name}" ];
           };
-        };
+        });
       in
       {
-        packages = { } // (package-and-docker "backend") // (package-and-docker "frontend");
+        packages = { }
+                   // (package-and-docker "backend" system)
+                   // (package-and-docker "frontend" system);
 
         devShells.default = pkgs.mkShell {
           buildInputs = builtins.attrValues {
diff --git a/nix/templates/goapp/frontend/db.go b/nix/templates/goapp/frontend/db.go
new file mode 100644
index 0000000..fd3605a
--- /dev/null
+++ b/nix/templates/goapp/frontend/db.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+	"database/sql"
+	"log"
+
+	_ "github.com/mattn/go-sqlite3"
+)
+
+const create string = `
+CREATE TABLE IF NOT EXISTS users (
+	id INTEGER NOT NULL PRIMARY KEY,
+	created_at DATETIME NOT NULL,
+	name TEXT,
+	passwordHash TEXT
+);
+`
+
+type State struct {
+	db       *sql.DB      // the database storing the "business data"
+	sessions *SqliteStore // the database storing sessions
+}
+
+func NewState() (*State, error) {
+	db, err := sql.Open("sqlite3", databasePath)
+	if err != nil {
+		log.Println("Error opening the db: ", err)
+		return nil, err
+	}
+	if _, err := db.Exec(create); err != nil {
+		log.Println("Error creating the tables: ", err)
+		return nil, err
+	}
+	return &State{
+		db: db,
+	}, nil
+}
diff --git a/nix/templates/goapp/frontend/default.nix b/nix/templates/goapp/frontend/default.nix
index 1a3aeeb..f4ed3a4 100644
--- a/nix/templates/goapp/frontend/default.nix
+++ b/nix/templates/goapp/frontend/default.nix
@@ -1,21 +1,16 @@
-{ pkgs, packagename, ... }:
+{ pkgs, name, ... }:
 
 let
   version = "0.0.1";
 in
 pkgs.buildGoModule {
-  name = "${packagename}-${version}";
-  pname = "${packagename}";
+  name = "${name}-${version}";
+  pname = "${name}";
   version = "${version}";
 
   src = ./.;
-  subPackages = [ "src" ];
-  vendorHash = "sha256-8wYERVt3PIsKkarkwPu8Zy/Sdx43P6g2lz2xRfvTZ2E=";
-
-  postInstall = ''
-    mkdir -p $out
-    mv $out/bin/src $out/bin/${packagename}
-  '';
+  subPackages = [ "" ];
+  vendorHash = "sha256-tIk8lmyuVETrOW7fA7K7uNNXAAtJAYSM4uH+xZaMWqc=";
 
   doCheck = true;
 }
diff --git a/nix/templates/goapp/frontend/go.mod b/nix/templates/goapp/frontend/go.mod
index 41401c9..1532a66 100644
--- a/nix/templates/goapp/frontend/go.mod
+++ b/nix/templates/goapp/frontend/go.mod
@@ -1,5 +1,25 @@
-module github.com/hanemile/goapp/backend
+module github.com/hanemile/goapp/frontend
 
 go 1.23.5
 
-require github.com/gorilla/mux v1.8.1
+require (
+	github.com/gorilla/handlers v1.5.2
+	github.com/gorilla/mux v1.8.1
+	github.com/gorilla/securecookie v1.1.2
+	github.com/gorilla/sessions v1.4.0
+	modernc.org/sqlite v1.34.5
+)
+
+require (
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/felixge/httpsnoop v1.0.4 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mattn/go-sqlite3 v1.14.24 // indirect
+	github.com/ncruces/go-strftime v0.1.9 // indirect
+	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+	golang.org/x/sys v0.22.0 // indirect
+	modernc.org/libc v1.55.3 // indirect
+	modernc.org/mathutil v1.6.0 // indirect
+	modernc.org/memory v1.8.0 // indirect
+)
diff --git a/nix/templates/goapp/frontend/go.sum b/nix/templates/goapp/frontend/go.sum
index 7128337..53d3f31 100644
--- a/nix/templates/goapp/frontend/go.sum
+++ b/nix/templates/goapp/frontend/go.sum
@@ -1,2 +1,59 @@
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
+github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
+github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
+github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
 github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
 github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
+github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
+github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
+github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
+golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
+golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
+modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
+modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
+modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
+modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
+modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
+modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
+modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
+modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
+modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
+modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
+modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
+modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
+modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
+modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
+modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
+modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/nix/templates/goapp/frontend/log.go b/nix/templates/goapp/frontend/log.go
new file mode 100644
index 0000000..5af719a
--- /dev/null
+++ b/nix/templates/goapp/frontend/log.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+	"net/http"
+	"os"
+
+	"github.com/gorilla/handlers"
+)
+
+// Defines a middleware containing a logfile
+//
+// This is done to combine gorilla/handlers with gorilla/mux middlewares to
+// just use r.Use(logger.Middleware) once instead of adding this to all
+// handlers manually (Yes, I'm really missing macros in Go...)
+type loggingMiddleware struct {
+	logFile *os.File
+}
+
+func (l *loggingMiddleware) Middleware(next http.Handler) http.Handler {
+	return handlers.LoggingHandler(l.logFile, next)
+}
+
+func authMiddleware(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		session, _ := globalState.sessions.Get(r, "session")
+		username := session.Values["username"]
+
+		if username == nil {
+			http.Redirect(w, r, "/login", http.StatusSeeOther)
+		} else {
+			next.ServeHTTP(w, r)
+		}
+	})
+}
diff --git a/nix/templates/goapp/frontend/main.go b/nix/templates/goapp/frontend/main.go
new file mode 100644
index 0000000..f6605be
--- /dev/null
+++ b/nix/templates/goapp/frontend/main.go
@@ -0,0 +1,80 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"time"
+
+	"github.com/gorilla/mux"
+)
+
+var (
+	host          string
+	port          int
+	logFilePath   string
+	databasePath  string
+	sessiondbPath string
+	globalState   *State
+)
+
+func initFlags() {
+	flag.StringVar(&host, "host", "127.0.0.1", "The host to listen on")
+	flag.StringVar(&host, "h", "127.0.0.1", "The host to listen on (shorthand)")
+
+	flag.IntVar(&port, "port", 8080, "The port to listen on")
+	flag.IntVar(&port, "p", 8080, "The port to listen on (shorthand)")
+
+	flag.StringVar(&logFilePath, "logfilepath", "./server.log", "The path to the log file")
+	flag.StringVar(&databasePath, "databasepath", "./main.db", "The path to the main database")
+	flag.StringVar(&sessiondbPath, "sessiondbpath", "./sessions.db", "The path to the session database")
+}
+
+func indexHandler(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprintf(w, "Hello World from the frontend")
+}
+
+func main() {
+	initFlags()
+	flag.Parse()
+
+	// log init
+	log.Println("[i] Setting up logging...")
+	logFile, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0664)
+	if err != nil {
+		log.Fatal("Error opening the server.log file: ", err)
+	}
+	logger := loggingMiddleware{logFile}
+
+	// db init
+	log.Println("[i] Setting up Global State Struct...")
+	s, err := NewState()
+	if err != nil {
+		log.Fatal("Error creating the NewState(): ", err)
+	}
+	globalState = s
+
+	// session init
+	log.Println("[i] Setting up Session Storage...")
+	store, err := NewSqliteStore(sessiondbPath, "sessions", "/", 3600, []byte(os.Getenv("SESSION_KEY")))
+	if err != nil {
+		panic(err)
+	}
+	globalState.sessions = store
+
+	r := mux.NewRouter()
+	r.Use(logger.Middleware)
+	r.HandleFunc("/", indexHandler)
+
+	srv := &http.Server{
+		Handler:      r,
+		Addr:         ":8080",
+		WriteTimeout: 15 * time.Second,
+		ReadTimeout:  15 * time.Second,
+	}
+
+	log.Printf("[i] Running the server on %s", srv.Addr)
+	log.Fatal(srv.ListenAndServe())
+}
diff --git a/nix/templates/goapp/frontend/sqlitestore.go b/nix/templates/goapp/frontend/sqlitestore.go
new file mode 100644
index 0000000..6f59d15
--- /dev/null
+++ b/nix/templates/goapp/frontend/sqlitestore.go
@@ -0,0 +1,284 @@
+/*
+	Gorilla Sessions backend for SQLite.
+
+Copyright (c) 2013 Contributors. See the list of contributors in the CONTRIBUTORS file for details.
+
+This software is licensed under a MIT style license available in the LICENSE file.
+*/
+package main
+
+import (
+	"database/sql"
+	"encoding/gob"
+	"errors"
+	"fmt"
+	"log"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/gorilla/securecookie"
+	"github.com/gorilla/sessions"
+	_ "modernc.org/sqlite"
+)
+
+type SqliteStore struct {
+	db         DB
+	stmtInsert *sql.Stmt
+	stmtDelete *sql.Stmt
+	stmtUpdate *sql.Stmt
+	stmtSelect *sql.Stmt
+
+	Codecs  []securecookie.Codec
+	Options *sessions.Options
+	table   string
+}
+
+type sessionRow struct {
+	id         string
+	data       string
+	createdOn  time.Time
+	modifiedOn time.Time
+	expiresOn  time.Time
+}
+
+type DB interface {
+	Exec(query string, args ...interface{}) (sql.Result, error)
+	Prepare(query string) (*sql.Stmt, error)
+	Close() error
+}
+
+func init() {
+	gob.Register(time.Time{})
+}
+
+func NewSqliteStore(endpoint string, tableName string, path string, maxAge int, keyPairs ...[]byte) (*SqliteStore, error) {
+	db, err := sql.Open("sqlite3", endpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewSqliteStoreFromConnection(db, tableName, path, maxAge, keyPairs...)
+}
+
+func NewSqliteStoreFromConnection(db DB, tableName string, path string, maxAge int, keyPairs ...[]byte) (*SqliteStore, error) {
+	// Make sure table name is enclosed.
+	tableName = "`" + strings.Trim(tableName, "`") + "`"
+
+	cTableQ := "CREATE TABLE IF NOT EXISTS " +
+		tableName + " (id INTEGER PRIMARY KEY, " +
+		"session_data LONGBLOB, " +
+		"created_on TIMESTAMP DEFAULT 0, " +
+		"modified_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
+		"expires_on TIMESTAMP DEFAULT 0);"
+	if _, err := db.Exec(cTableQ); err != nil {
+		return nil, err
+	}
+
+	insQ := "INSERT INTO " + tableName +
+		"(id, session_data, created_on, modified_on, expires_on) VALUES (NULL, ?, ?, ?, ?)"
+	stmtInsert, stmtErr := db.Prepare(insQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	delQ := "DELETE FROM " + tableName + " WHERE id = ?"
+	stmtDelete, stmtErr := db.Prepare(delQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	updQ := "UPDATE " + tableName + " SET session_data = ?, created_on = ?, expires_on = ? " +
+		"WHERE id = ?"
+	stmtUpdate, stmtErr := db.Prepare(updQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	selQ := "SELECT id, session_data, created_on, modified_on, expires_on from " +
+		tableName + " WHERE id = ?"
+	stmtSelect, stmtErr := db.Prepare(selQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	return &SqliteStore{
+		db:         db,
+		stmtInsert: stmtInsert,
+		stmtDelete: stmtDelete,
+		stmtUpdate: stmtUpdate,
+		stmtSelect: stmtSelect,
+		Codecs:     securecookie.CodecsFromPairs(keyPairs...),
+		Options: &sessions.Options{
+			Path:   path,
+			MaxAge: maxAge,
+		},
+		table: tableName,
+	}, nil
+}
+
+func (m *SqliteStore) Close() {
+	m.stmtSelect.Close()
+	m.stmtUpdate.Close()
+	m.stmtDelete.Close()
+	m.stmtInsert.Close()
+	m.db.Close()
+}
+
+func (m *SqliteStore) Get(r *http.Request, name string) (*sessions.Session, error) {
+	return sessions.GetRegistry(r).Get(m, name)
+}
+
+func (m *SqliteStore) New(r *http.Request, name string) (*sessions.Session, error) {
+	session := sessions.NewSession(m, name)
+	session.Options = &sessions.Options{
+		Path:   m.Options.Path,
+		MaxAge: m.Options.MaxAge,
+	}
+	session.IsNew = true
+	var err error
+	if cook, errCookie := r.Cookie(name); errCookie == nil {
+		err = securecookie.DecodeMulti(name, cook.Value, &session.ID, m.Codecs...)
+		if err == nil {
+			err = m.load(session)
+			if err == nil {
+				session.IsNew = false
+			} else {
+				err = nil
+			}
+		}
+	}
+	return session, err
+}
+
+func (m *SqliteStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
+	var err error
+	if session.ID == "" {
+		if err = m.insert(session); err != nil {
+			return err
+		}
+	} else if err = m.save(session); err != nil {
+		return err
+	}
+	encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, m.Codecs...)
+	if err != nil {
+		return err
+	}
+	http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options))
+	return nil
+}
+
+func (m *SqliteStore) insert(session *sessions.Session) error {
+	var createdOn time.Time
+	var modifiedOn time.Time
+	var expiresOn time.Time
+	crOn := session.Values["created_on"]
+	if crOn == nil {
+		createdOn = time.Now()
+	} else {
+		createdOn = crOn.(time.Time)
+	}
+	modifiedOn = createdOn
+	exOn := session.Values["expires_on"]
+	if exOn == nil {
+		expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
+	} else {
+		expiresOn = exOn.(time.Time)
+	}
+	delete(session.Values, "created_on")
+	delete(session.Values, "expires_on")
+	delete(session.Values, "modified_on")
+
+	encoded, encErr := securecookie.EncodeMulti(session.Name(), session.Values, m.Codecs...)
+	if encErr != nil {
+		return encErr
+	}
+	res, insErr := m.stmtInsert.Exec(encoded, createdOn, modifiedOn, expiresOn)
+	if insErr != nil {
+		return insErr
+	}
+	lastInserted, lInsErr := res.LastInsertId()
+	if lInsErr != nil {
+		return lInsErr
+	}
+	session.ID = fmt.Sprintf("%d", lastInserted)
+	return nil
+}
+
+func (m *SqliteStore) Delete(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
+
+	// Set cookie to expire.
+	options := *session.Options
+	options.MaxAge = -1
+	http.SetCookie(w, sessions.NewCookie(session.Name(), "", &options))
+	// Clear session values.
+	for k := range session.Values {
+		delete(session.Values, k)
+	}
+
+	_, delErr := m.stmtDelete.Exec(session.ID)
+	if delErr != nil {
+		return delErr
+	}
+	return nil
+}
+
+func (m *SqliteStore) save(session *sessions.Session) error {
+	if session.IsNew == true {
+		return m.insert(session)
+	}
+	var createdOn time.Time
+	var expiresOn time.Time
+	crOn := session.Values["created_on"]
+	if crOn == nil {
+		createdOn = time.Now()
+	} else {
+		createdOn = crOn.(time.Time)
+	}
+
+	exOn := session.Values["expires_on"]
+	if exOn == nil {
+		expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
+		log.Print("nil")
+	} else {
+		expiresOn = exOn.(time.Time)
+		if expiresOn.Sub(time.Now().Add(time.Second*time.Duration(session.Options.MaxAge))) < 0 {
+			expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
+		}
+	}
+
+	delete(session.Values, "created_on")
+	delete(session.Values, "expires_on")
+	delete(session.Values, "modified_on")
+	encoded, encErr := securecookie.EncodeMulti(session.Name(), session.Values, m.Codecs...)
+	if encErr != nil {
+		return encErr
+	}
+	_, updErr := m.stmtUpdate.Exec(encoded, createdOn, expiresOn, session.ID)
+	if updErr != nil {
+		return updErr
+	}
+	return nil
+}
+
+func (m *SqliteStore) load(session *sessions.Session) error {
+	row := m.stmtSelect.QueryRow(session.ID)
+	sess := sessionRow{}
+	scanErr := row.Scan(&sess.id, &sess.data, &sess.createdOn, &sess.modifiedOn, &sess.expiresOn)
+	if scanErr != nil {
+		return scanErr
+	}
+	if sess.expiresOn.Sub(time.Now()) < 0 {
+		log.Printf("Session expired on %s, but it is %s now.", sess.expiresOn, time.Now())
+		return errors.New("Session expired")
+	}
+	err := securecookie.DecodeMulti(session.Name(), sess.data, &session.Values, m.Codecs...)
+	if err != nil {
+		return err
+	}
+	session.Values["created_on"] = sess.createdOn
+	session.Values["modified_on"] = sess.modifiedOn
+	session.Values["expires_on"] = sess.expiresOn
+	return nil
+
+}
diff --git a/nix/templates/goapp/frontend/src/main.go b/nix/templates/goapp/frontend/src/main.go
deleted file mode 100644
index adb15cd..0000000
--- a/nix/templates/goapp/frontend/src/main.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"log"
-	"net/http"
-	"time"
-
-	"github.com/gorilla/mux"
-)
-
-func indexHandler(w http.ResponseWriter, r *http.Request) {
-	fmt.Fprintf(w, "Hello World from the frontend")
-}
-
-func main() {
-	r := mux.NewRouter()
-	r.HandleFunc("/", indexHandler)
-
-	srv := &http.Server{
-		Handler:      r,
-		Addr:         ":8080",
-		WriteTimeout: 15 * time.Second,
-		ReadTimeout:  15 * time.Second,
-	}
-
-	log.Printf("[i] Running the server on %s", srv.Addr)
-	log.Fatal(srv.ListenAndServe())
-}