Your resource for web content, online publishing
and the distribution of digital products.
S M T W T F S
 
 
 
 
 
1
 
2
 
3
 
4
 
5
 
6
 
7
 
8
 
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
 
27
 
28
 
29
 
30
 

How to Build SaaS Multi-Tenant Applications: Multi-Tenant SaaS in 200 Lines of Code

DATE POSTED:November 20, 2024

This is a complete SaaS example (Software-as-a-Service) using PostgreSQL as a database and Gliimly 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!

\ First, create a directory for your application where the source code will be:

mkdir -p notes cd notes Setup Postgres Database

Create a 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

\ Create a database configuration file to describe your PostgreSQL database above:

echo "user=$(whoami) dbname=db_app" > db_app

\ Create database objects we'll need - a users table for application users, and a 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 Create A Gliimly Application

Create the application "notes" owned by your Linux user:

sudo mgrg -i -u $(whoami) notes Source Code

This executes before any other handler in an application, making sure all requests are authorized, file "before-handler.gliim":

vi before-handler.gliim

\ Copy and paste:

before-handler set-param displayed_logout = false, is_logged_in = false call-handler "/session/check" end-before-handler

Signup users, login, logout

\ This is a generic session management web service that handles user creation, verification, login, and logout. Create file "session.gliim":

vi session.gliim

\ Copy and paste:

// Display link to login or signup %% /session/login-or-signup private @>">Login     >">Sign Up
%% // Login with email and password, and create a new session, then display home pag %% /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. <>
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. <>
%% // Starting point of the application. Either display login form or a home page: %% /session/start public get-param action, is_logged_in type bool 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" %% // Generic home page, you can call anything from here, in this case a list of note %% /session/show-home private call-handler "/notes/list" %% // Logout user and display home, which will ask to either login or signup %% /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.
commit-transaction @db_app end-if end-if call-handler "/session/show-home" %% // Check session based on session cookie. If session cookie corresponds to the email address, the request is a part of an authorized session %% /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 if-true action not-equal "logout" @Hi <>! >">Logout
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 %% // Check that email verification token is the one actually sent to the email address %% /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 >">Login. 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 >">again. exit-handler %% // Display login form that asks for email and password %% /session/user/login public call-handler "/session/login-or-signup" @Please Login:
@
>" method="POST"> @ @ @ @
%% // Display form for a new user, asking for an email and password %% /session/user/new/form public @Create New User
@
>" method="POST"> @ @ @ @
%% // Send verification email %% /session/user/new/send-verify private get-param email, verify write-string msg @From: [email protected] @To: <> @Subject: verify your account @ @Your verification code is: <> 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 <>, code is <> set-param verify_sent = false else-if set-param verify_sent = true end-if %% // Create new user from email and password %% /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 %% // Display form to enter the code emailed to user to verify the email address %% /session/user/new/verify-form public get-param email @Please check your email and enter verification code here: @
>" method="POST"> @ @ @ @
%%

Notes application

This is the actual application that uses the above session management services. Create file "notes.gliim":

vi notes.gliim

\ Copy and paste:

// Delete a note %% /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 (<>) end-if %% // Display a form to add a note %% /notes/form-add public call-handler "/notes/header" @Add New Note @
>" method="POST"> @ @ @
%% // Add a note %% /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 (<>) end-if %% // List all notes %% /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 "
\n" result with_breaks status st cache if-true st equal 0 set-string with_breaks = note end-if @Date: <> (>?note_id=<>">delete note)
@Note: <>
@
end-query %% // Display a question whether to delete a note or not %% /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 >?note_id=<>">delete note now. %% // Check if session is authorized, and display an appropriate header %% /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 @

Welcome to Notes!


if-true is_logged_in equal false exit-handler end-if @>">Add Note >">List Notes
%% Build Application gg -q --db=postgres:db_app Run Web Services Application Server mgrg notes Emailing

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

\ and on Fedora systems (like RedHat):

sudo dnf install postfix sudo systemctl start postfix

\ When the application sends an email to a local user, such as @localhost, then you can see the email sent at:

sudo vi /var/mail/ Setup Nginx

A web server sits in front of the Gliimly application server, so it needs to be set up. This example is for Ubuntu, so edit the Nginx config file there:

sudo vi /etc/nginx/sites-enabled/default

\ Add this in the "server {}" section:

location /notes/ { include /etc/nginx/fastcgi_params; fastcgi_pass unix:///var/lib/gg/notes/sock/sock; }

\ Restart Nginx:

sudo systemctl restart nginx You're Done; Run It!

Go to your web browser, and enter:

http://127.0.0.1/notes/session/start

\