Multi-tenant SaaS (Notes web application) in 200 lines of code
This is a complete SaaS example (Software-as-a-Service) using PostgreSQL as a database, and Golf as a web service engine; it includes user signup/login/logout with an email and password, separate user accounts and data, and a notes application. All in about 200 lines of code! Here's a video that shows it - two users sign up and create their own notes.
First create a directory for your application, where the source code will be:
mkdir -p notes
cd notes
Copied!
Create PostgreSQL user (with the same name as your logged on Linux user, so no password needed), and the database "db_app":
echo "create user $(whoami);
create database db_app with owner=$(whoami);
grant all on database db_app to $(whoami);
\q" | sudo -u postgres psql
Copied!
Create a database configuration file to describe your PostgreSQL database above:
echo "user=$(whoami) dbname=db_app" > db_app
Copied!
Create database objects we'll need - users table for application users, and notes table to hold their notes:
echo "create table if not exists notes (dateOf timestamp, noteId bigserial primary key, userId bigint, note varchar(1000));
create table if not exists users (userId bigserial primary key, email varchar(100), hashed_pwd varchar(100), verified smallint, verify_token varchar(30), session varchar(100));
create unique index if not exists users1 on users (email);" | psql -d db_app
Copied!
Create application "notes" owned by your Linux user:
mgrg -i -u $(whoami) notes
Copied!
This executes before any other handler in an application, making sure all requests are authorized, file "before-handler.golf":
vi before-handler.golf
Copied!
Copy and paste:
before-handler
set-param displayed_logout = false, is_logged_in = false
call-handler "/session/check"
end-before-handler
Copied!
- Signup users, login, logout
This is a generic session management web service that handles user creation, verification, login and logout. Create file "session.golf":
vi session.golf
Copied!
Copy and paste:
%% /session/login-or-signup private
@<a href="<<print-path "/session/user/login">>">Login</a> <a href="<<print-path "/session/user/new/form">>">Sign Up</a><hr/>
%%
%% /session/login public
get-param pwd, email
hash-string pwd to hashed_pwd
random-string to sess_id length 30
run-query @db_app = "select userId from users where email='%s' and hashed_pwd='%s'" output sess_user_id : email, hashed_pwd
run-query @db_app no-loop = "update users set session='%s' where userId='%s'" input sess_id, sess_user_id affected-rows arows
if-true arows not-equal 1
@Could not create a session. Please try again. <<call-handler "/session/login-or-signup">> <hr/>
exit-handler
end-if
set-cookie "sess_user_id" = sess_user_id path "/", "sess_id" = sess_id path "/"
call-handler "/session/check"
call-handler "/session/show-home"
exit-handler
end-query
@Email or password are not correct. <<call-handler "/session/login-or-signup">><hr/>
%%
%% /session/start public
get-param action default-value "", is_logged_in type bool default-value false
if-true is_logged_in equal true
if-true action not-equal "logout"
call-handler "/session/show-home"
exit-handler
end-if
end-if
call-handler "/session/user/login"
%%
%% /session/show-home private
call-handler "/notes/list"
%%
%% /session/logout public
get-param is_logged_in type bool
if-true is_logged_in equal true
get-param sess_user_id
run-query @db_app = "update users set session='' where userId='%s'" input sess_user_id no-loop affected-rows arows
if-true arows equal 1
set-param is_logged_in = false
@You have been logged out.<hr/>
commit-transaction @db_app
end-if
end-if
call-handler "/session/show-home"
%%
%% /session/check private
get-cookie sess_user_id="sess_user_id", sess_id="sess_id"
set-param sess_id, sess_user_id
if-true sess_id not-equal ""
set-param is_logged_in = false
run-query @db_app = "select email from users where userId='%s' and session='%s'" output email input sess_user_id, sess_id row-count rcount
set-param is_logged_in = true
get-param displayed_logout type bool
if-true displayed_logout equal false
get-param action default-value ""
if-true action not-equal "logout"
@Hi <<print-out email>>! <a href="<<print-path "/session/logout">>">Logout</a><br/>
end-if
set-param displayed_logout = true
end-if
end-query
if-true rcount not-equal 1
set-param is_logged_in = false
end-if
end-if
%%
%% /session/verify-signup public
get-param code, email
run-query @db_app = "select verify_token from users where email='%s'" output db_verify : email
if-true code equal db_verify
@Your email has been verifed. Please <a href="<<print-path "/session/user/login">>">Login</a>.
run-query @db_app no-loop = "update users set verified=1 where email='%s'" : email
exit-handler
end-if
end-query
@Could not verify the code. Please try <a href="<<print-path "/session/user/new/verify-form">>">again</a>.
exit-handler
%%
%% /session/user/login public
call-handler "/session/login-or-signup"
@Please Login:<hr/>
@<form action="<<print-path "/session/login">>" method="POST">
@<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email">
@<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password">
@<button type="submit">Go</button>
@</form>
%%
%% /session/user/new/form public
@Create New User<hr/>
@<form action="<<print-path "/session/user/new/create">>" method="POST">
@<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email">
@<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password">
@<input type="submit" value="Sign Up">
@</form>
%%
%% /session/user/new/send-verify private
get-param email, verify
write-string msg
@From: service@your-service.com
@To: <<print-out email>>
@Subject: verify your account
@
@Your verification code is: <<print-out verify>>
end-write-string
exec-program "/usr/sbin/sendmail" args "-i", "-t" input msg status st
if-true st not-equal 0 or true equal false
@Could not send email to <<print-out email>>, code is <<print-out verify>>
set-param verify_sent = false
else-if
set-param verify_sent = true
end-if
%%
%% /session/user/new/create public
get-param email, pwd
hash-string pwd to hashed_pwd
random-string to verify length 5 number
begin-transaction @db_app
run-query @db_app no-loop = "insert into users (email, hashed_pwd, verified, verify_token, session) values ('%s', '%s', '0', '%s', '')" input email, hashed_pwd, verify affected-rows arows error err on-error-continue
if-true err not-equal "0" or arows not-equal 1
call-handler "/session/login-or-signup"
@User with this email already exists.
rollback-transaction @db_app
else-if
set-param email, verify
call-handler "/session/user/new/send-verify"
get-param verify_sent type bool
if-true verify_sent equal false
rollback-transaction @db_app
exit-handler
end-if
commit-transaction @db_app
call-handler "/session/user/new/verify-form"
end-if
%%
%% /session/user/new/verify-form public
get-param email
@Please check your email and enter verification code here:
@<form action="<<print-path "/session/verify-signup">>" method="POST">
@<input name="email" type="hidden" value="<<print-out email>>">
@<input name="code" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Verification code">
@<button type="submit">Verify</button>
@</form>
%%
Copied!
- Notes application
This is the actual application that uses above session management services. Create file "notes.golf":
vi notes.golf
Copied!
Copy and paste:
%% /notes/delete public
call-handler "/notes/header"
get-param sess_user_id, note_id
run-query @db_app = "delete from notes where noteId='%s' and userId='%s'" : note_id, sess_user_id \
affected-rows arows no-loop error errnote
if-true arows equal 1
@Note deleted
else-if
@Could not delete note (<<print-out errnote>>)
end-if
%%
%% /notes/form-add public
call-handler "/notes/header"
@Add New Note
@<form action="<<print-path "/notes/add">>" method="POST">
@<textarea name="note" rows="5" cols="50" required autofocus placeholder="Enter Note"></textarea>
@<button type="submit">Create</button>
@</form>
%%
%% /notes/add public
call-handler "/notes/header"
get-param note, sess_user_id
run-query @db_app = "insert into notes (dateOf, userId, note) values (now(), '%s', '%s')" : sess_user_id, note \
affected-rows arows no-loop error errnote
if-true arows equal 1
@Note added
else-if
@Could not add note (<<print-out errnote>>)
end-if
%%
%% /notes/list public
call-handler "/notes/header"
get-param sess_user_id
run-query @db_app = "select dateOf, note, noteId from notes where userId='%s' order by dateOf desc" \
input sess_user_id output dateOf, note, noteId
match-regex "\n" in note replace-with "<br/>\n" result with_breaks status st cache
if-true st equal 0
set-string with_breaks = note
end-if
@Date: <<print-out dateOf>> (<a href="<<print-path "/notes/ask-delete">>?note_id=<<print-out noteId>>">delete note</a>)<br/>
@Note: <<print-out with_breaks>><br/>
@<hr/>
end-query
%%
%% /notes/ask-delete public
call-handler "/notes/header"
get-param note_id
@Are you sure you want to delete a note? Use Back button to go back,\
or <a href="<<print-path "/notes/delete">>?note_id=<<print-out note_id>>">delete note now</a>.
%%
%% /notes/header private
get-param is_logged_in type bool
if-true is_logged_in equal false
call-handler "/session/login-or-signup"
end-if
@<h1>Welcome to Notes!</h1><hr/>
if-true is_logged_in equal false
exit-handler
end-if
@<a href="<<print-path "/notes/form-add">>">Add Note</a> <a href="<<print-path "/notes/list">>">List Notes</a><hr/>
%%
Copied!
gg -q --db=postgres:db_app
Copied!
Run web services application server
mgrg notes
Copied!
In order to use this example, you need to be able to email local users, which means email addresses such as \"myuser@localhost\". To do that, install postfix (or sendmail). On Debian systems (like Ubuntu):
sudo apt install postfix
sudo systemctl start postfix
Copied!
and on Fedora systems (like RedHat):
sudo dnf install postfix
sudo systemctl start postfix
Copied!
When the application sends an email to a local user, such as <OS user>@localhost, then you can see the email sent at:
sudo vi /var/mail/<OS user>
Copied!
A web server sits in front of Golf application server, so it needs to be setup. This example is for Ubuntu, so edit Nginx config file there:
sudo vi /etc/nginx/sites-enabled/default
Copied!
Add this in "server {}" section (replace "your-user" with your OS user name):
location /notes/ { include /etc/nginx/fastcgi_params; fastcgi_pass unix:///home/your-user/.golf/apps/notes/sock/.sock; }
Copied!
Restart Nginx:
sudo systemctl restart nginx
Copied!
Go to your web browser, and enter:
http://127.0.0.1/notes/session/start
Copied!
Articles
article-capi
article-cookies
article-debug
article-distributed
article-encryption
article-fetch-web-page
article-fifo
article-file-manager
article-hello-server
article-hello-world
article-hello-world-service
article-hello-world-service-web
article-how-to-create-golf-application
article-json
article-language
article-mariadb
article-memory-safety
article-memory-safety-web
article-notes-postgres
article-random
article-regex
article-remote-call
article-request-function
article-security
article-sendmail
article-server
article-shopping
article-sqlite
article-statements
article-status-check
article-tree
article-tree-web
article-vim-coloring
article-web-framework-for-c-programming-language
article-what-is-golf
article-what-is-web-service
See all
documentation
Copyright (c) 2019-2025 Gliim LLC. All contents on this web site is "AS IS" without warranties or guarantees of any kind.