{"id":68431,"date":"2022-11-07T09:00:31","date_gmt":"2022-11-07T09:00:31","guid":{"rendered":"https:\/\/www.cryptocabaret.com\/?p=68431"},"modified":"2022-11-07T09:00:31","modified_gmt":"2022-11-07T09:00:31","slug":"build-your-own-saas-on-linux-with-vely","status":"publish","type":"post","link":"https:\/\/www.cryptocabaret.com\/?p=68431","title":{"rendered":"Build your own SaaS on Linux with Vely"},"content":{"rendered":"<p><span class=\"field field--name-title field--type-string field--label-hidden\">Build your own SaaS on Linux with Vely<\/span><br \/>\n<span class=\"field field--name-uid field--type-entity-reference field--label-hidden\"><a title=\"View user profile.\" href=\"https:\/\/opensource.com\/users\/vely\" class=\"username\">Sergio Mijatovic<\/a><\/span><br \/>\n<span class=\"field field--name-created field--type-created field--label-hidden\">Mon, 11\/07\/2022 &#8211; 03:00<\/span><\/p>\n<div class=\"clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item\">\n<p><a href=\"https:\/\/opensource.com\/article\/22\/5\/write-c-appplications-vely-linux\" target=\"_blank\" rel=\"noopener\">Vely<\/a> combines high performance and the low footprint of C with the ease of use and improved safety of languages like PHP. It&#8217;s free and open source software, licensed under GPLv3 and LGPL 3 for libraries, so you can even build commercial software with it.<\/p>\n<h2>Using Vely for SaaS<\/h2>\n<p>You can use Vely to create a multitenant web application that you can run on the Internet as Software-as-a-Service (SaaS). Each user has a completely separate data space from any other.<\/p>\n<p>In this example web application, a user can sign up for a notebook service to create notes and then view and delete them. It demonstrates several technology integrations in just 310 lines of code across seven source files. The technologies include:<\/p>\n<ul>\n<li>MariaDB<\/li>\n<li>Web browser<\/li>\n<li>Apache<\/li>\n<li>Unix sockets<\/li>\n<\/ul>\n<h3>How it works<\/h3>\n<p>Here&#8217;s how the application works from a user&#8217;s perspective. A code walk-through follows the images.<\/p>\n<p>The app allows a user to create a new login by specifying an email address and password. You can style these any way you like, such as with CSS:<\/p>\n<article class=\"align-center media media--type-image media--view-mode-default\">\n<div class=\"field field--name-field-media-image field--type-image field--label-hidden field__item\">  <img decoding=\"async\" loading=\"lazy\" src=\"https:\/\/www.cryptocabaret.com\/wp-content\/uploads\/2022\/11\/1createuser.png\" width=\"1428\" height=\"194\" alt=\"Create a user account\"><\/div>\n<div class=\"field field--name-field-caption field--type-text-long field--label-hidden caption field__item\"><span class=\"caption__byline\">Image by: <\/span><\/p>\n<p>(Sergio Mijatovic, CC BY-SA 4.0)<\/p>\n<\/div>\n<\/article>\n<p>Verify the user&#8217;s email:<\/p>\n<article class=\"align-center media media--type-image media--view-mode-default\">\n<div class=\"field field--name-field-media-image field--type-image field--label-hidden field__item\">  <img decoding=\"async\" loading=\"lazy\" src=\"https:\/\/www.cryptocabaret.com\/wp-content\/uploads\/2022\/11\/2verifyemail.png\" width=\"780\" height=\"144\" alt=\"Verify the user's email address\"><\/div>\n<div class=\"field field--name-field-caption field--type-text-long field--label-hidden caption field__item\"><span class=\"caption__byline\">Image by: <\/span><\/p>\n<p>(Sergio Mijatovic, CC BY-SA 4.0)<\/p>\n<\/div>\n<\/article>\n<p>Each user logs in with their unique username and password:<\/p>\n<article class=\"align-center media media--type-image media--view-mode-default\">\n<div class=\"field field--name-field-media-image field--type-image field--label-hidden field__item\">  <img decoding=\"async\" loading=\"lazy\" src=\"https:\/\/www.cryptocabaret.com\/wp-content\/uploads\/2022\/11\/3login.png\" width=\"1386\" height=\"228\" alt=\"The user logs in\"><\/div>\n<div class=\"field field--name-field-caption field--type-text-long field--label-hidden caption field__item\"><span class=\"caption__byline\">Image by: <\/span><\/p>\n<p>(Sergio Mijatovic, CC BY-SA 4.0)<\/p>\n<\/div>\n<\/article>\n<p>Once logged in, a user can add a note:<\/p>\n<article class=\"align-center media media--type-image media--view-mode-default\">\n<div class=\"field field--name-field-media-image field--type-image field--label-hidden field__item\">  <img decoding=\"async\" loading=\"lazy\" src=\"https:\/\/www.cryptocabaret.com\/wp-content\/uploads\/2022\/11\/4addnote.png\" width=\"784\" height=\"476\" alt=\"The user can add a note\"><\/div>\n<div class=\"field field--name-field-caption field--type-text-long field--label-hidden caption field__item\"><span class=\"caption__byline\">Image by: <\/span><\/p>\n<p>(Sergio Mijatovic, CC BY-SA 4.0)<\/p>\n<\/div>\n<\/article>\n<p>A user can get a list of notes:<\/p>\n<article class=\"align-center media media--type-image media--view-mode-default\">\n<div class=\"field field--name-field-media-image field--type-image field--label-hidden field__item\">  <img decoding=\"async\" loading=\"lazy\" src=\"https:\/\/www.cryptocabaret.com\/wp-content\/uploads\/2022\/11\/5listnotes.png\" width=\"792\" height=\"550\" alt=\"User lists notes\"><\/div>\n<div class=\"field field--name-field-caption field--type-text-long field--label-hidden caption field__item\"><span class=\"caption__byline\">Image by: <\/span><\/p>\n<p>(Sergio Mijatovic, CC BY-SA 4.0)<\/p>\n<\/div>\n<\/article>\n<p>The app asks for confirmation before deleting a note:<\/p>\n<article class=\"align-center media media--type-image media--view-mode-default\">\n<div class=\"field field--name-field-media-image field--type-image field--label-hidden field__item\">  <img decoding=\"async\" loading=\"lazy\" src=\"https:\/\/www.cryptocabaret.com\/wp-content\/uploads\/2022\/11\/6confirmdelete.png\" width=\"992\" height=\"368\" alt=\"The app asks for confirmation before deleting a note\"><\/div>\n<div class=\"field field--name-field-caption field--type-text-long field--label-hidden caption field__item\"><span class=\"caption__byline\">Image by: <\/span><\/p>\n<p>(Sergio Mijatovic, CC BY-SA 4.0)<\/p>\n<\/div>\n<\/article>\n<p>After the user confirms, the note is deleted:<\/p>\n<article class=\"align-center media media--type-image media--view-mode-default\">\n<div class=\"field field--name-field-media-image field--type-image field--label-hidden field__item\">  <img decoding=\"async\" loading=\"lazy\" src=\"https:\/\/www.cryptocabaret.com\/wp-content\/uploads\/2022\/11\/7notedeleted.png\" width=\"922\" height=\"358\" alt=\"After confirmation, the note is deleted\"><\/div>\n<div class=\"field field--name-field-caption field--type-text-long field--label-hidden caption field__item\"><span class=\"caption__byline\">Image by: <\/span><\/p>\n<p>(Sergio Mijatovic, CC BY-SA 4.0)<\/p>\n<\/div>\n<\/article>\n<h3>Setup prerequisites<\/h3>\n<p>Follow the installation instructions on <a href=\"https:\/\/vely.dev\/\" target=\"_blank\" rel=\"noopener\">Vely.dev<\/a>. It&#8217;s a quick process that uses standard packaging tools, such as DNF, APT, Pacman, or Zypper.<\/p>\n<p>Because they are part of this example, you must install Apache as a web server and MariaDB as a database.<\/p>\n<p>After installing Vely, turn on syntax highlighting in Vim if you&#8217;re using it:<\/p>\n<pre>\n<span class=\"geshifilter\"><code class=\"text geshifilter-text\">vv -m<\/code><\/span><\/pre>\n<h3>Get the source code<\/h3>\n<p>The source code for this demonstration SaaS app is part of the Vely installation. It&#8217;s a good idea to create a separate source code directory for each application (and you can name it whatever you like). In this case, unpacking the source code does that for you:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"bash geshifilter-bash\">$ <span class=\"kw2\">tar<\/span> xvf $<span class=\"br0\">(<\/span>vv -o<span class=\"br0\">)<\/span><span class=\"sy0\">\/<\/span>examples<span class=\"sy0\">\/<\/span>multitenant_SaaS.tar.gz<br>\n$ <span class=\"kw3\">cd<\/span> multitenant_SaaS<\/div><\/div><\/pre>\n<p>By default, the application is named <code>multitenant_SaaS<\/code>, but you can call it anything (if you do that, change it everywhere).<\/p>\n<h2>Set up the application<\/h2>\n<p>The very first step is to create an application. It&#8217;s simple to do with Vely&#8217;s <code>vf<\/code> utility:<\/p>\n<pre>\n<span class=\"geshifilter\"><code class=\"bash geshifilter-bash\"><span class=\"co4\">$ <\/span><span class=\"kw2\">sudo<\/span> vf <span class=\"re5\">-i<\/span> <span class=\"re5\">-u<\/span> $<span class=\"br0\">(<\/span><span class=\"kw2\">whoami<\/span><span class=\"br0\">)<\/span> multitenant_SaaS<\/code><\/span><\/pre>\n<p>This command creates a new application home (<code>\/var\/lib\/vv\/multitenant_SaaS<\/code>) and performs the application setup for you. Mostly, that means creating various subdirectories in the home folder and assigning privileges. In this case, only the current user (the result of <code>whoami<\/code>) owns the directories, with 0700 privileges, which ensures that no one else has access to the files.<\/p>\n<h2>Set up the database<\/h2>\n<p>Before doing any coding, you need a place to store the information used by the application. First, create a MariaDB database called <code>db_multitenant_SaaS<\/code>, owned by the user <code>vely<\/code> with password <code>your_password<\/code>. You can change any of these values, but remember to change them everywhere during this example.<\/p>\n<p>Logged in as root in the MySQL utility:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"sql geshifilter-sql\"><span class=\"kw1\">CREATE<\/span> <span class=\"kw1\">DATABASE<\/span> <span class=\"kw1\">IF<\/span> <span class=\"kw1\">NOT<\/span> <span class=\"kw1\">EXISTS<\/span> db_multitenant_SaaS;<br><span class=\"kw1\">CREATE<\/span> <span class=\"kw1\">USER<\/span> <span class=\"kw1\">IF<\/span> <span class=\"kw1\">NOT<\/span> <span class=\"kw1\">EXISTS<\/span> vely <span class=\"kw1\">IDENTIFIED<\/span> <span class=\"kw1\">BY<\/span> <span class=\"st0\">'your_password'<\/span>;<br><span class=\"kw1\">GRANT<\/span> <span class=\"kw1\">CREATE<\/span><span class=\"sy0\">,<\/span><span class=\"kw1\">ALTER<\/span><span class=\"sy0\">,<\/span><span class=\"kw1\">DROP<\/span><span class=\"sy0\">,<\/span><span class=\"kw1\">SELECT<\/span><span class=\"sy0\">,<\/span><span class=\"kw1\">INSERT<\/span><span class=\"sy0\">,<\/span><span class=\"kw1\">DELETE<\/span><span class=\"sy0\">,<\/span><span class=\"kw1\">UPDATE<\/span> <span class=\"kw1\">ON<\/span> db_multitenant_SaaS<span class=\"sy0\">.*<\/span> <span class=\"kw1\">TO<\/span> vely;<\/div><\/div><\/pre>\n<p>Then create database objects (tables and records and so on) in the database:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"sql geshifilter-sql\"><span class=\"kw1\">USE<\/span> db_multitenant_SaaS;<br><span class=\"kw1\">SOURCE<\/span> setup<span class=\"sy0\">.<\/span>sql;<br>\nexit<\/div><\/div><\/pre>\n<h2>Connect Vely to a database<\/h2>\n<p>To let Vely know where your database is and how to log into it, create a database config file named <code>db_multitenant_SaaS<\/code>. (This is the name used by the database statements in the source code, so if you change it, make sure you change it everywhere.)<\/p>\n<p>Vely uses native MariaDB database connectivity, so you can specify any options that a given database lets you:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"bash geshifilter-bash\">$ <span class=\"kw3\">echo<\/span> <span class=\"st_h\">'[client]<br>\nuser=vely<br>\npassword=your_password<br>\ndatabase=db_multitenant_SaaS<br>\nprotocol=TCP<br>\nhost=127.0.0.1<br>\nport=3306'<\/span> <span class=\"sy0\">&gt;<\/span> db_multitenant_SaaS<\/div><\/div><\/pre>\n<h2>Build the application<\/h2>\n<p>Use the <code>vv<\/code> utility to make the application, using the <code>--db<\/code> option to specify the MariaDB database and the database config file:<\/p>\n<pre>\n<span class=\"geshifilter\"><code class=\"bash geshifilter-bash\"><span class=\"co4\">$ <\/span>vv <span class=\"re5\">-q<\/span> <span class=\"re5\">--db<\/span>=mariadb:db_multitenant_SaaS<\/code><\/span><\/pre>\n<\/p>\n<div class=\"embedded-resource-list callout-float-right\">\n<div class=\"field field--name-title field--type-string field--label-hidden field__item\">Programming and development<\/div>\n<div class=\"field field--name-links field--type-link field--label-hidden field__items\">\n<div class=\"field__item\"><a href=\"https:\/\/developers.redhat.com\/?intcmp=7016000000127cYAAQ\">Red Hat Developers Blog<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/downloads\/cheat-sheets?intcmp=7016000000127cYAAQ\">Programming cheat sheets<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/www.redhat.com\/en\/services\/training\/learning-subscription?intcmp=7016000000127cYAAQ\">Try for free: Red Hat Learning Subscription<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/downloads\/bash-programming-guide?intcmp=7016000000127cYAAQ\">eBook: An introduction to programming with Bash<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/developers.redhat.com\/cheat-sheets\/bash-shell-cheat-sheet?intcmp=7016000000127cYAAQ\">Bash shell scripting cheat sheet<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/developers.redhat.com\/e-books\/modernizing-enterprise-java?intcmp=7016000000127cYAAQ\">eBook: Modernizing Enterprise Java<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/downloads\/building-applications?intcmp=7016000000127cYAAQ\">An open source developer&#8217;s guide to building applications<\/a><\/div>\n<\/p><\/div>\n<\/p><\/div>\n<h2>Start the application server<\/h2>\n<p>To start the application server for your web application, use the <code>vf<\/code> FastCGI process manager. The application server uses a Unix socket to communicate with the web server (creating a reverse proxy):<\/p>\n<pre>\n<span class=\"geshifilter\"><code class=\"bash geshifilter-bash\"><span class=\"co4\">$ <\/span>vf <span class=\"re5\">-w<\/span> <span class=\"nu0\">3<\/span> multitenant_SaaS<\/code><\/span><\/pre>\n<p>This starts three daemon processes to serve the incoming requests. You can also start an adaptive server that increases the number of processes to serve more requests and gradually reduce the number of processes when they&#8217;re not needed:<\/p>\n<pre>\n<span class=\"geshifilter\"><code class=\"bash geshifilter-bash\"><span class=\"co4\">$ <\/span>vf multitenant_SaaS<\/code><\/span><\/pre>\n<p>See <code>vf<\/code> for more options to help you achieve the best performance.<\/p>\n<p>When you need to stop your application server, use the <code>-m quit<\/code> option:<\/p>\n<pre>\n<span class=\"geshifilter\"><code class=\"bash geshifilter-bash\"><span class=\"co4\">$ <\/span>vf <span class=\"re5\">-m<\/span> quit multitenant_SaaS<\/code><\/span><\/pre>\n<h2>Set up the web server<\/h2>\n<p>This is a web application, so the application needs a web server. This example uses Apache by way of a Unix socket listener.<\/p>\n<h3>1. Set up Apache<\/h3>\n<p>To configure Apache as a reverse proxy and connect your application to it, you need to enable FastCGI proxy support, which generally means using the <code>proxy<\/code> and <code>proxy_fcgi<\/code> modules.<\/p>\n<p>For Fedora systems (or others, like Arch) enable the <code>proxy<\/code> and <code>proxy_fcgi<\/code> modules by adding (or uncommenting) the appropriate <strong>LoadModule<\/strong> directives in the <code>\/etc\/httpd\/conf\/httpd.conf<\/code> Apache configuration file.<\/p>\n<p>For Debian, Ubuntu, and similar systems, enable the <code>proxy<\/code> and <code>proxy_fcgi<\/code> modules:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"bash geshifilter-bash\">$ <span class=\"kw2\">sudo<\/span> a2enmod proxy<br>\n$ <span class=\"kw2\">sudo<\/span> a2enmod proxy_fcgi<\/div><\/div><\/pre>\n<p>For OpenSUSE, add these lines to the end of <code>\/etc\/apache2\/httpd.conf<\/code>:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"text geshifilter-text\">LoadModule proxy_module modules\/mod_proxy.so<br>\nLoadModule proxy_fcgi_module modules\/mod_proxy_fcgi.so<\/div><\/div><\/pre>\n<h3>2. Configure Apache<\/h3>\n<p>Now you must add the proxy information to the Apache configuration file:<\/p>\n<pre>\n<span class=\"geshifilter\"><code class=\"text geshifilter-text\">ProxyPass \"\/multitenant_SaaS\" unix:\/\/\/var\/lib\/vv\/multitenant_SaaS\/sock\/sock|fcgi:\/\/localhost\/multitenant_SaaS<\/code><\/span><\/pre>\n<p>The location of your configuration may vary, depending on your Linux distribution:<\/p>\n<ul>\n<li>Fedora, CentOS, Mageia, and Arch: <code>\/etc\/httpd\/conf\/httpd.conf<\/code><\/li>\n<li>Debian, Ubuntu, Mint: <code>\/etc\/apache2\/apache2.conf<\/code><\/li>\n<li>OpenSUSE: <code>\/etc\/apache2\/httpd.conf<\/code><\/li>\n<\/ul>\n<h3>3. Restart<\/h3>\n<p>Finally, restart Apache. On Fedora and similar systems, as well as Arch Linux:<\/p>\n<pre>\n<span class=\"geshifilter\"><code class=\"bash geshifilter-bash\"><span class=\"co4\">$ <\/span><span class=\"kw2\">sudo<\/span> systemctl restart httpd<\/code><\/span><\/pre>\n<p>On Debian and Debian-based systems, as well as OpenSUSE:<\/p>\n<pre>\n<span class=\"geshifilter\"><code class=\"bash geshifilter-bash\"><span class=\"co4\">$ <\/span><span class=\"kw2\">sudo<\/span> systemctl restart apache2<\/code><\/span><\/pre>\n<h2>Set up local mail<\/h2>\n<p>This example uses email as a part of its function. If your server can already send email, you can skip this. Otherwise, you can use local mail (<code>myuser@localhost<\/code>) just to test it out. To do that, install Sendmail.<\/p>\n<p>On Fedora and similar:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"bash geshifilter-bash\">$ <span class=\"kw2\">sudo<\/span> dnf <span class=\"kw2\">install<\/span> <span class=\"kw2\">sendmail<\/span><br>\n$ <span class=\"kw2\">sudo<\/span> systemctl start <span class=\"kw2\">sendmail<\/span><\/div><\/div><\/pre>\n<p>On Debian systems (like Ubuntu):<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"bash geshifilter-bash\">$ <span class=\"kw2\">sudo<\/span> apt <span class=\"kw2\">install<\/span> <span class=\"kw2\">sendmail<\/span><br>\n$ <span class=\"kw2\">sudo<\/span> systemctl start <span class=\"kw2\">sendmail<\/span><\/div><\/div><\/pre>\n<p>When the application sends an email to a local user, such as <code>OS_user@localhost<\/code>, then you can verify that the email was sent by looking at <code>\/var\/mail\/<os_user code=\"\"><\/os_user><\/code> (the &#8220;mail spool&#8221;).<\/p>\n<h2>Access the application server from the browser<\/h2>\n<p>Assuming you&#8217;re running the application locally, use <code>http:\/\/127.0.0.1\/multitenant_SaaS?req=notes&amp;action=begin<\/code> to access your application server from your web browser. If you&#8217;re running this on a live server on the Internet, you may need to adjust your firewall settings to allow HTTP traffic.<\/p>\n<h2>Source code<\/h2>\n<p>This example application contains seven source files. You can review the code yourself (remember, it&#8217;s just 310 lines across these files), but here&#8217;s an overview of each one.<\/p>\n<h3>SQL setup (setup.sql)<\/h3>\n<p>The two tables created are:<\/p>\n<ul>\n<li><strong>users<\/strong>: Information about each user. Each user in the <strong>users<\/strong> table has its own unique ID (<strong>userId<\/strong> column) along with other information such as email address and whether it&#8217;s verified. There&#8217;s also a hashed password. An actual password is never stored in plain text (or otherwise); a one-way hash is used to check the password.<\/li>\n<li><strong>notes<\/strong>: Notes entered by the user. The <strong>notes<\/strong> table contains the notes, each along with <strong>userId<\/strong> column that states which user owns them. The <strong>userId<\/strong> column&#8217;s value matches the namesake column from <strong>users<\/strong> table. This way, every note clearly belongs to a single user.<\/li>\n<\/ul>\n<p>The file contents:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"sql geshifilter-sql\"><span class=\"kw1\">CREATE<\/span> <span class=\"kw1\">TABLE<\/span> <span class=\"kw1\">IF<\/span> <span class=\"kw1\">NOT<\/span> <span class=\"kw1\">EXISTS<\/span> notes <span class=\"br0\">(<\/span>dateOf datetime<span class=\"sy0\">,<\/span> noteId <span class=\"kw1\">BIGINT<\/span> <span class=\"kw1\">AUTO_INCREMENT<\/span> <span class=\"kw1\">PRIMARY<\/span> <span class=\"kw1\">KEY<\/span><span class=\"sy0\">,<\/span> userId <span class=\"kw1\">BIGINT<\/span><span class=\"sy0\">,<\/span> note <span class=\"kw1\">VARCHAR<\/span><span class=\"br0\">(<\/span><span class=\"nu0\">1000<\/span><span class=\"br0\">)<\/span><span class=\"br0\">)<\/span>;<br><span class=\"kw1\">CREATE<\/span> <span class=\"kw1\">TABLE<\/span> <span class=\"kw1\">IF<\/span> <span class=\"kw1\">NOT<\/span> <span class=\"kw1\">EXISTS<\/span> users <span class=\"br0\">(<\/span>userId <span class=\"kw1\">BIGINT<\/span> <span class=\"kw1\">AUTO_INCREMENT<\/span> <span class=\"kw1\">PRIMARY<\/span> <span class=\"kw1\">KEY<\/span><span class=\"sy0\">,<\/span> email <span class=\"kw1\">VARCHAR<\/span><span class=\"br0\">(<\/span><span class=\"nu0\">100<\/span><span class=\"br0\">)<\/span><span class=\"sy0\">,<\/span> hashed_pwd <span class=\"kw1\">VARCHAR<\/span><span class=\"br0\">(<\/span><span class=\"nu0\">100<\/span><span class=\"br0\">)<\/span><span class=\"sy0\">,<\/span> verified <span class=\"kw1\">SMALLINT<\/span><span class=\"sy0\">,<\/span> verify_token <span class=\"kw1\">VARCHAR<\/span><span class=\"br0\">(<\/span><span class=\"nu0\">30<\/span><span class=\"br0\">)<\/span><span class=\"sy0\">,<\/span> <span class=\"kw1\">SESSION<\/span> <span class=\"kw1\">VARCHAR<\/span><span class=\"br0\">(<\/span><span class=\"nu0\">100<\/span><span class=\"br0\">)<\/span><span class=\"br0\">)<\/span>;<br><span class=\"kw1\">CREATE<\/span> <span class=\"kw1\">UNIQUE<\/span> <span class=\"kw1\">INDEX<\/span> <span class=\"kw1\">IF<\/span> <span class=\"kw1\">NOT<\/span> <span class=\"kw1\">EXISTS<\/span> users1 <span class=\"kw1\">ON<\/span> users <span class=\"br0\">(<\/span>email<span class=\"br0\">)<\/span>;<\/div><\/div><\/pre>\n<h3>Run-time data (login.h)<\/h3>\n<p>To properly display the Login, Sign Up, and Logout links, you need some flags that are available anywhere in the application. Also, the application uses cookies to maintain a session, so this needs to be available anywhere, for example, to verify that the session is valid. Every request sent to the application is confirmed that way. Only requests that come with verifiable cookies are permitted.<\/p>\n<p>So to that effect, you have a <strong>global_request_data<\/strong> type <code>reqdata<\/code> (request data) and in it there&#8217;s <code>sess_userId<\/code> (ID of user) and <code>sess_id<\/code> (user&#8217;s current session ID). You also have rather self-explanatory flags that help render pages:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"text geshifilter-text\">#ifndef _VV_LOGIN<br>\n#define _VV_LOGIN<br><br>\ntypedef struct s_reqdata {<br>\n\u00a0 \u00a0 bool displayed_logout; \/\/ true if Logout link displayed<br>\n\u00a0 \u00a0 bool is_logged_in; \/\/ true if session verified logged-in<br>\n\u00a0 \u00a0 char *sess_userId; \/\/ user ID of current session<br>\n\u00a0 \u00a0 char *sess_id; \/\/ session ID<br>\n} reqdata;<br><br>\nvoid login_or_signup ();<br><br>\n#endif<\/div><\/div><\/pre>\n<h3>Session checking and session data (_before.vely)<\/h3>\n<p>Vely has a notion of a <strong>before_request_handler<\/strong>. The code you write executes before any other code that handles a request. To do this, all you need is to write this code in a file named <code>_before.vely<\/code>, and the rest is automatically handled.<\/p>\n<p>Anything that a SaaS application does, such as handling requests sent to an application, must be validated for security. This way, the application knows whether the caller has the permissions needed to perform an action.<\/p>\n<p>Checking for permission is done here in a before-request handler. That way, whatever other code you have handling a request, you already have the session information.<\/p>\n<p>To keep session data (like session ID and user ID) available anywhere in your code, you use <strong>global_request_data<\/strong>. It&#8217;s just a generic pointer (<strong>void*<\/strong>) to memory that any code that handles requests can access. This is perfect for handling sessions, as shown below:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"text geshifilter-text\">#include \"vely.h\"<br>\n#include \"login.h\"<br><br>\n\/\/ _before() is a before-request-handler. It always executes before<br>\n\/\/ any other code that handles a request. It's a good place for any<br>\n\/\/ kind of request-wide setting or data initialization<br>\nvoid _before() {<br>\n\u00a0 \u00a0 \/\/ Output HTTP header<br>\n\u00a0 \u00a0 out-header default<br>\n\u00a0 \u00a0 reqdata *rd; \/\/ this is global request data, see login.h<br>\n\u00a0 \u00a0 \/\/ allocate memory for global request data, will be automatically deallocated<br>\n\u00a0 \u00a0 \/\/ at the end of request<br>\n\u00a0 \u00a0 new-mem rd size sizeof(reqdata)<br>\n\u00a0 \u00a0 \/\/ initialize flags<br>\n\u00a0 \u00a0 rd-&gt;displayed_logout = false;<br>\n\u00a0 \u00a0 rd-&gt;is_logged_in = false;<br>\n\u00a0 \u00a0 \/\/ set the data we created to be global request data, accessible<br>\n\u00a0 \u00a0 \/\/ from any code that handles a request<br>\n\u00a0 \u00a0 set-req data rd<br>\n\u00a0 \u00a0 \/\/ check if session exists (based on cookies from the client)<br>\n\u00a0 \u00a0 \/\/ this executes before any other request-handling code, making it<br>\n\u00a0 \u00a0 \/\/ easier to just have session information ready<br>\n\u00a0 \u00a0 _check_session ();<br>\n}<\/div><\/div><\/pre>\n<h3>Checking if the session is valid (_check_session.vely)<\/h3>\n<p>One of the most important tasks in a multitenant SaaS application is to check (as soon as possible) if the session is valid by checking whether a user is logged in. It&#8217;s done by getting the session ID and user ID cookies from the client (such as a web browser) and checking these against the database where sessions are stored:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"text geshifilter-text\">#include \"vely.h\"<br>\n#include \"login.h\"<br><br><br>\n\/\/ Check if session is valid<br>\nvoid _check_session () {<br>\n\u00a0 \u00a0 \/\/ Get global request data<br>\n\u00a0 \u00a0 reqdata *rd;<br>\n\u00a0 \u00a0 get-req data to rd<br>\n\u00a0 \u00a0 \/\/ Get cookies from user browser<br>\n\u00a0 \u00a0 get-cookie rd-&gt;sess_userId=\"sess_userId\"<br>\n\u00a0 \u00a0 get-cookie rd-&gt;sess_id=\"sess_id\"<br>\n\u00a0 \u00a0 if (rd-&gt;sess_id[0] != 0) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ Check if session ID is correct for given user ID<br>\n\u00a0 \u00a0 \u00a0 \u00a0 char *email;<br>\n\u00a0 \u00a0 \u00a0 \u00a0 run-query @db_multitenant_SaaS = \"select email from users where userId='%s' and session='%s'\" output email : rd-&gt;sess_userId, rd-&gt;sess_id row-count define rcount<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 query-result email to email<br>\n\u00a0 \u00a0 \u00a0 \u00a0 end-query<br>\n\u00a0 \u00a0 \u00a0 \u00a0 if (rcount == 1) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ if correct, set logged-in flag<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 rd-&gt;is_logged_in = true;<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ if Logout link not display, then display it<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 if (rd-&gt;displayed_logout == false) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @Hi &gt;! <a href=\"https:\/\/opensource.com\/?req=login&amp;action=logout\">Logout<\/a><br><br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 rd-&gt;displayed_logout = true;<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 }<br>\n\u00a0 \u00a0 \u00a0 \u00a0 } else rd-&gt;is_logged_in = false;<br>\n\u00a0 \u00a0 }<br>\n}<\/div><\/div><\/pre>\n<h3>Signing up, Logging in, Logging out (login.vely)<\/h3>\n<p>The basis of any multitenant system is the ability for a user to sign up, log in, and log out. Typically, signing up involves verifying the email address; more often than not, the same email address is used as a username. That&#8217;s the case here.<\/p>\n<p>There are several subrequests implemented here that are necessary to perform the functionality:<\/p>\n<ul>\n<li>When Signing Up a new user, display the HTML form to collect the information. The URL request signature for this is <code>req=login&amp;action=newuser<\/code>.<\/li>\n<li>As a response to the Sign Up form, create a new user. The URL request signature is <code>req=login&amp;action=createuser<\/code>. The <strong>input-param<\/strong> signal obtains an <strong>email<\/strong> and <strong>pwd<\/strong> POST form fields. The password value is a one-way hash, and an email verification token is created as a random five-digit number. These are inserted into the <strong>users<\/strong> table, creating a new user. A verification email is sent, and the user is prompted to read the email and enter the code.<\/li>\n<li>Verify the email by entering the verification code sent to that email. The URL request signature is <code>req=login&amp;action=verify<\/code>.<\/li>\n<li>Display a Login form for the user to log in. The URL request signature is <code>req=login<\/code> (for instance, <code>action<\/code> is empty.)<\/li>\n<li>Log in by verifying the email address (username) and password. The URL request signature is <code>req=login&amp;action=login<\/code>.<\/li>\n<li>Logout at the user&#8217;s request. The URL request signature is <code>req=login&amp;action=logout<\/code>.<\/li>\n<li>Landing page for the application. The URL request signature is <code>req=login&amp;action=begin<\/code>.<\/li>\n<li>If the user is currently logged in, go to the application&#8217;s landing page.<\/li>\n<\/ul>\n<p>See examples of these below:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"text geshifilter-text\">#include \"vely.h\"<br>\n#include \"login.h\"<br><br>\n\/\/ Handle session maintenance, login, logout, session verification<br>\n\/\/ for any multitenant Cloud application<br>\nvoid login () {<br>\n\u00a0 \u00a0 \/\/ Get URL input parameter \"action\"<br>\n\u00a0 \u00a0 input-param action<br><br>\n\u00a0 \u00a0 \/\/ Get global request data, we record session information in it, so it's handy<br>\n\u00a0 \u00a0 reqdata *rd;<br>\n\u00a0 \u00a0 get-req data to rd<br><br>\n\u00a0 \u00a0 \/\/ If session is already established, the only reason why we won't proceed to<br>\n\u00a0 \u00a0 \/\/ application home is if we're logging out<br>\n\u00a0 \u00a0 if (rd-&gt;is_logged_in) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 if (strcmp(action, \"logout\")) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 _show_home();<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 exit-request<br>\n\u00a0 \u00a0 \u00a0 \u00a0 }<br>\n\u00a0 \u00a0 }<br><br>\n\u00a0 \u00a0 \/\/ Application screen to get started. Show links to login or signup and show<br>\n\u00a0 \u00a0 \/\/ home screen appropriate for this<br>\n\u00a0 \u00a0 if (!strcmp (action, \"begin\")) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 _show_home();<br>\n\u00a0 \u00a0 \u00a0 \u00a0 exit-request<br><br>\n\u00a0 \u00a0 \/\/ Start creating new user. Ask for email and password, then proceed to create user<br>\n\u00a0 \u00a0 \/\/ when this form is submitted.<br>\n\u00a0 \u00a0 } else if (!strcmp (action, \"newuser\")) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @Create New User<hr><br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<br><br>\n\u00a0 \u00a0 \/\/ Verify code sent to email by user. The code must match, thus verifying email address \u00a0 \u00a0<br>\n\u00a0 \u00a0 } else if (!strcmp (action, \"verify\")) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 input-param code<br>\n\u00a0 \u00a0 \u00a0 \u00a0 input-param email<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ Get verify token based on email<br>\n\u00a0 \u00a0 \u00a0 \u00a0 run-query @db_multitenant_SaaS = \"select verify_token from users where email='%s'\" output db_verify : email<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 query-result db_verify to define db_verify<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ Compare token recorded in database with what user provided<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 if (!strcmp (code, db_verify)) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @Your email has been verifed. Please <a href=\"https:\/\/opensource.com\/?req=login\">Login<\/a>.<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ If matches, update user info to indicate it's verified<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 run-query @db_multitenant_SaaS no-loop = \"update users set verified=1 where email='%s'\" : email<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 exit-request<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 }<br>\n\u00a0 \u00a0 \u00a0 \u00a0 end-query<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @Could not verify the code. Please try <a href=\"https:\/\/opensource.com\/?req=login\">again<\/a>.<br>\n\u00a0 \u00a0 \u00a0 \u00a0 exit-request<br><br>\n\u00a0 \u00a0 \/\/ Create user - this runs when user submits form with email and password to create a user \u00a0 \u00a0 <br>\n\u00a0 \u00a0 } else if (!strcmp (action, \"createuser\")) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 input-param email<br>\n\u00a0 \u00a0 \u00a0 \u00a0 input-param pwd<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ create hashed (one-way) password<br>\n\u00a0 \u00a0 \u00a0 \u00a0 hash-string pwd to define hashed_pwd<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ generate random 5 digit string for verify code<br>\n\u00a0 \u00a0 \u00a0 \u00a0 random-string to define verify length 5 number<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ create user: insert email, hashed password, verification token. Current verify status is 0, or not verified<br>\n\u00a0 \u00a0 \u00a0 \u00a0 begin-transaction @db_multitenant_SaaS<br>\n\u00a0 \u00a0 \u00a0 \u00a0 run-query @db_multitenant_SaaS no-loop = \"insert into users (email, hashed_pwd, verified, verify_token, session) values ('%s', '%s', '0', '%s', '')\" : email, hashed_pwd, verify affected-rows define arows error define err on-error-continue<br>\n\u00a0 \u00a0 \u00a0 \u00a0 if (strcmp (err, \"0\") || arows != 1) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ if cannot add user, it probably doesn't exist. Either way, we can't proceed.<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 login_or_signup();<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @User with this email already exists.<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 rollback-transaction @db_multitenant_SaaS<br>\n\u00a0 \u00a0 \u00a0 \u00a0 } else {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ Create email with verification code and email it to user<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 write-string define msg<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @From: vely@vely.dev<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @To: &gt;<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @Subject: verify your account<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @Your verification code is: &gt;<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 end-write-string<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 exec-program \"\/usr\/sbin\/sendmail\" args \"-i\", \"-t\" input msg status define st<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 if (st != 0) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @Could not send email to &gt;, code is &gt;<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 rollback-transaction @db_multitenant_SaaS<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 exit-request<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 }<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 commit-transaction @db_multitenant_SaaS<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ Inform the user to go check email and enter verification code<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @Please check your email and enter verification code here:<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @<button type=\"submit\">Verify<\/button><br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 }<br><br>\n\u00a0 \u00a0 \/\/ This runs when logged-in user logs out. \u00a0 \u00a0<br>\n\u00a0 \u00a0 } else if (!strcmp (action, \"logout\")) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ Update user table to wipe out session, meaning no such user is logged in<br>\n\u00a0 \u00a0 \u00a0 \u00a0 if (rd-&gt;is_logged_in) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 run-query @db_multitenant_SaaS = \"update users set session='' where userId='%s'\" : rd-&gt;sess_userId no-loop affected-rows define arows<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 if (arows == 1) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 rd-&gt;is_logged_in = false; \/\/ indicate user not logged in<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @You have been logged out.<hr><br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 }<br>\n\u00a0 \u00a0 \u00a0 \u00a0 }<br>\n\u00a0 \u00a0 \u00a0 \u00a0 _show_home();<br><br>\n\u00a0 \u00a0 \/\/ Login: this runs when user enters user name and password<br>\n\u00a0 \u00a0 } else if (!strcmp (action, \"login\")) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 input-param pwd<br>\n\u00a0 \u00a0 \u00a0 \u00a0 input-param email<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ create one-way hash with the intention of comparing with user table - password is NEVER recorded<br>\n\u00a0 \u00a0 \u00a0 \u00a0 hash-string pwd to define hashed_pwd<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ create random 30-long string for session ID<br>\n\u00a0 \u00a0 \u00a0 \u00a0 random-string to rd-&gt;sess_id length 30<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ Check if user name and hashed password match<br>\n\u00a0 \u00a0 \u00a0 \u00a0 run-query @db_multitenant_SaaS = \"select userId from users where email='%s' and hashed_pwd='%s'\" output sess_userId : email, hashed_pwd<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 query-result sess_userId to rd-&gt;sess_userId<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ If match, update user table with session ID<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 run-query @db_multitenant_SaaS no-loop = \"update users set session='%s' where userId='%s'\" : rd-&gt;sess_id, rd-&gt;sess_userId affected-rows define arows<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 if (arows != 1) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @Could not create a session. Please try again. &gt; <hr><br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 exit-request<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 }<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ Set user ID and session ID as cookies. User's browser will return those to us with every request<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 set-cookie \"sess_userId\" = rd-&gt;sess_userId<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 set-cookie \"sess_id\" = rd-&gt;sess_id<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ Display home, make sure session is correct first and set flags<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 _check_session();<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 _show_home();<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 exit-request<br>\n\u00a0 \u00a0 \u00a0 \u00a0 end-query<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @Email or password are not correct. &gt;<hr><br><br>\n\u00a0 \u00a0 \/\/ Login screen, asks user to enter user name and password \u00a0 \u00a0<br>\n\u00a0 \u00a0 } else if (!strcmp (action, \"\")) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 login_or_signup();<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @Please Login:<hr><br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<button type=\"submit\">Go<\/button><br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 }<br>\n}<br><br>\n\/\/ Display Login or Sign Up links<br>\nvoid login_or_signup() {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<a href=\"https:\/\/opensource.com\/?req=login\">Login<\/a> &amp; &amp; <a href=\"https:\/\/opensource.com\/?req=login&amp;action=newuser\">Sign Up<\/a><hr><br>\n}<\/div><\/div><\/pre>\n<h3>General-purpose application (_show_home.vely)<\/h3>\n<p>With this tutorial, you can create any multitenant SaaS application you want. The multitenant-processing module above (<code>login.vely<\/code>) calls the <strong>_show_home()<\/strong> function, which can house any code of yours. This example code shows the Notes application, but it could be anything. The <strong>_show_home()<\/strong> function calls any code you wish and is a general-purpose multitenant application plug-in:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"text geshifilter-text\">#include \"vely.h\"<br><br>\nvoid _show_home() {<br>\n\u00a0 \u00a0 notes();<br>\n\u00a0 \u00a0 exit-request<br>\n}<\/div><\/div><\/pre>\n<h3>Notes application (notes.vely)<\/h3>\n<p>The application is able to add, list, and delete any given note:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"text geshifilter-text\">#include \"vely.h\"<br>\n#include \"login.h\"<br><br>\n\/\/ Notes application in a multitenant Cloud<br>\nvoid notes () {<br>\n\u00a0 \u00a0 \/\/ get global request data<br>\n\u00a0 \u00a0 reqdata *rd;<br>\n\u00a0 \u00a0 get-req data to rd<br>\n\u00a0 \u00a0 \/\/ If session invalid, display Login or Signup<br>\n\u00a0 \u00a0 if (!rd-&gt;is_logged_in) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 login_or_signup();<br>\n\u00a0 \u00a0 }<br>\n\u00a0 \u00a0 \/\/ Greet the user<br>\n\u00a0 \u00a0 @<h1>Welcome to Notes!<\/h1><hr><br>\n\u00a0 \u00a0 \/\/ If not logged in, exit - this ensures security verification of user's identity<br>\n\u00a0 \u00a0 if (!rd-&gt;is_logged_in) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 exit-request<br>\n\u00a0 \u00a0 }<br>\n\u00a0 \u00a0 \/\/ Get URL parameter that tells Notes what to do<br>\n\u00a0 \u00a0 input-param subreq<br>\n\u00a0 \u00a0 \/\/ Display actions that Notes can do (add or list notes)<br>\n\u00a0 \u00a0 @<a href=\"https:\/\/opensource.com\/?req=notes&amp;subreq=add\">Add Note<\/a> <a href=\"https:\/\/opensource.com\/?req=notes&amp;subreq=list\">List Notes<\/a><hr><br><br>\n\u00a0 \u00a0 \/\/ List all notes for this user<br>\n\u00a0 \u00a0 if (!strcmp (subreq, \"list\")) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ select notes for this user ONLY<br>\n\u00a0 \u00a0 \u00a0 \u00a0 run-query @db_multitenant_SaaS = \"select dateOf, note, noteId from notes where userId='%s' order by dateOf desc\" : rd-&gt;sess_userId output dateOf, note, noteId<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 query-result dateOf to define dateOf<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 query-result note to define note<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 query-result noteId to define noteId<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ change new lines to <br> with fast cached Regex<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 match-regex \"n\" in note replace-with \"<br>n\" result define with_breaks status define st cache<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 if (st == 0) with_breaks = note; \/\/ nothing was found\/replaced, just use original<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ Display a note<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @Date: &gt; (<a href=\"https:\/\/opensource.com\/?req=notes&amp;subreq=delete_note_ask&amp;note_id=%3C%3Cp-out%20noteId%3E%3E\">delete note<\/a>)<br><br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @Note: &gt;<br><br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @<hr><br>\n\u00a0 \u00a0 \u00a0 \u00a0 end-query<br>\n\u00a0 \u00a0 }<br><br>\n\u00a0 \u00a0 \/\/ Ask to delete a note<br>\n\u00a0 \u00a0 else if (!strcmp (subreq, \"delete_note_ask\")) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 input-param note_id<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @Are you sure you want to delete a note? Use Back button to go back, or <a href=\"https:\/\/opensource.com\/?req=notes&amp;subreq=delete_note&amp;note_id=%3C%3Cp-out%20note_id%3E%3E\">delete note now<\/a>.<br>\n\u00a0 \u00a0 }<br><br>\n\u00a0 \u00a0 \/\/ Delete a note<br>\n\u00a0 \u00a0 else if (!strcmp (subreq, \"delete_note\")) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 input-param note_id<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ Delete note<br>\n\u00a0 \u00a0 \u00a0 \u00a0 run-query @db_multitenant_SaaS = \"delete from notes where noteId='%s' and userId='%s'\" : note_id, rd-&gt;sess_userId affected-rows define arows no-loop error define errnote<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ Inform user of status<br>\n\u00a0 \u00a0 \u00a0 \u00a0 if (arows == 1) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @Note deleted<br>\n\u00a0 \u00a0 \u00a0 \u00a0 } else {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @Could not delete note (&gt;)<br>\n\u00a0 \u00a0 \u00a0 \u00a0 }<br>\n\u00a0 \u00a0 }<br><br>\n\u00a0 \u00a0 \/\/ Add a note<br>\n\u00a0 \u00a0 else if (!strcmp (subreq, \"add_note\")) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ Get URL POST data from note form<br>\n\u00a0 \u00a0 \u00a0 \u00a0 input-param note<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ Insert note under this user's ID<br>\n\u00a0 \u00a0 \u00a0 \u00a0 run-query @db_multitenant_SaaS = \"insert into notes (dateOf, userId, note) values (now(), '%s', '%s')\" : rd-&gt;sess_userId, note affected-rows define arows no-loop error define errnote<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ Inform user of status<br>\n\u00a0 \u00a0 \u00a0 \u00a0 if (arows == 1) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @Note added<br>\n\u00a0 \u00a0 \u00a0 \u00a0 } else {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 @Could not add note (&gt;)<br>\n\u00a0 \u00a0 \u00a0 \u00a0 }<br>\n\u00a0 \u00a0 }<br><br>\n\u00a0 \u00a0 \/\/ Display an HTML form to collect a note, and send it back here (with subreq=\"add_note\" URL param)<br>\n\u00a0 \u00a0 else if (!strcmp (subreq, \"add\")) {<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @Add New Note<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<textarea name=\"note\" rows=\"5\" cols=\"50\" required autofocus placeholder=\"Enter Note\"><\/textarea><br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<button type=\"submit\">Create<\/button><br>\n\u00a0 \u00a0 \u00a0 \u00a0 @<br>\n\u00a0 \u00a0 }<br>\n}<\/div><\/div><\/pre>\n<h2>SaaS with C performance<\/h2>\n<p>Vely makes it possible to leverage the power of C in your web applications. A multitenant SaaS application is a prime example of a use case that benefits from that. Take a look at the code examples, write some code, and give Vely a try.<\/p>\n<\/div>\n<div class=\"clearfix text-formatted field field--name-field-article-subhead field--type-text-long field--label-hidden field__item\">\n<p>Vely makes it possible to leverage the power of C in your web applications.<\/p>\n<\/div>\n<div class=\"field field--name-field-lead-image field--type-entity-reference field--label-hidden field__item\">\n<article class=\"media media--type-image media--view-mode-caption\">\n<div class=\"field field--name-field-media-image field--type-image field--label-hidden field__item\">  <img decoding=\"async\" loading=\"lazy\" src=\"https:\/\/www.cryptocabaret.com\/wp-content\/uploads\/2022\/11\/BUSINESS_buildtogether.png\" width=\"520\" height=\"292\" alt=\"People working together to build \" title=\"People working together to build \"><\/div>\n<div class=\"field field--name-field-caption field--type-text-long field--label-hidden caption field__item\"><span class=\"caption__byline\">Image by: <\/span><\/p>\n<p>Opensource.com<\/p>\n<\/div>\n<\/article>\n<\/div>\n<div class=\"field field--name-field-tags field--type-entity-reference field--label-hidden field__items\">\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/tags\/databases\" hreflang=\"en\">Databases<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/tags\/programming\" hreflang=\"en\">Programming<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/tags\/linux\" hreflang=\"en\">Linux<\/a><\/div>\n<\/p><\/div>\n<div class=\"hidden field field--name-field-listicle-title field--type-string field--label-hidden field__item\">What to read next<\/div>\n<div class=\"field field--name-field-default-license field--type-list-string field--label-hidden field__item\"><a rel=\"license\" href=\"http:\/\/creativecommons.org\/licenses\/by-sa\/4.0\/\"><br \/>\n        <img decoding=\"async\" alt=\"Creative Commons License\" src=\"https:\/\/www.cryptocabaret.com\/wp-content\/uploads\/2022\/11\/cc-by-sa-4-7.png\" title=\"This work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.\"><\/a>This work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.<\/div>\n<section class=\"field field--name-field-comments field--type-comment field--label-hidden comment-wrapper\">\n<div class=\"comments__count\">\n<div class=\"login\"><a href=\"https:\/\/opensource.com\/user\/register?absolute=1\">Register<\/a> or <a href=\"https:\/\/opensource.com\/user\/login?destination=\/feed&amp;absolute=1\">Login<\/a> to post a comment.<\/div>\n<\/p><\/div>\n<\/section>\n<p class=\"wpematico_credit\"><small>Powered by <a href=\"http:\/\/www.wpematico.com\" target=\"_blank\" rel=\"noopener\">WPeMatico<\/a><\/small><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Build your own SaaS on Linux with Vely Sergio Mijatovic Mon, 11\/07\/2022 &#8211; 03:00 Vely combines high performance and the low footprint of C with the ease of use and improved safety of languages like PHP. It&#8217;s free and open source software, licensed under GPLv3 and LGPL 3 for libraries, so you can even build [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":68432,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[307],"tags":[],"class_list":["post-68431","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-open-source"],"_links":{"self":[{"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/posts\/68431","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=68431"}],"version-history":[{"count":0,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/posts\/68431\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/media\/68432"}],"wp:attachment":[{"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=68431"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=68431"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=68431"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}