diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..7b724693cb1228ac01c4054b648888b95c32cace
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+data/data.sqlite
+gmnifaq.conf
diff --git a/LICENSE b/LICENSE
new file mode 100755
index 0000000000000000000000000000000000000000..027d71d0073f90db1ea80f7524f6fceccb001e93
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2018-2020, René Wagner
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a62cea7fb42d100a02f22b60bc623d0f2e965aca
--- /dev/null
+++ b/README.md
@@ -0,0 +1,17 @@
+# gmnifaq
+
+gmnifaq is going to be a simple, self-hostable FAQ-engine for the [gemini protocol](gemini://gemini.circumlunar.space)
+
+## planned features
+
+- support for tags
+- searching
+- view by tag
+- view all
+
+## requirements
+
+- gemini server with cgi enabled
+- Perl >= 5.28
+- SQLite
+
diff --git a/data/data.sqlite.example b/data/data.sqlite.example
new file mode 100644
index 0000000000000000000000000000000000000000..d8c4059aa1104544e7399cafb62f38c9b210290e
Binary files /dev/null and b/data/data.sqlite.example differ
diff --git a/gmnifaq.conf.example b/gmnifaq.conf.example
new file mode 100644
index 0000000000000000000000000000000000000000..2c0c2be11d38c80d85179ba3d324c1437390c9d5
--- /dev/null
+++ b/gmnifaq.conf.example
@@ -0,0 +1,2 @@
+$sitename = 'gemini faq';
+$siteintro = 'Welcome to gmnifaq, the simple cgi engine for your gemini capsule';
diff --git a/index.pl b/index.pl
new file mode 100755
index 0000000000000000000000000000000000000000..d7865c082337d027e890cf73f9ebb8fbc7c91d58
--- /dev/null
+++ b/index.pl
@@ -0,0 +1,97 @@
+#!/usr/bin/perl
+# Copyright René Wagner 2020
+# licenced under BSD 3-Clause licence
+# https://git.sr.ht/~rwa/gmni-perl-cgi-demo
+
+use strict;
+use DBI;
+
+# define return codes
+our %RC = (
+ 'INPUT', 10,
+ 'SENSITIVE_INPUT', 11,
+ 'SUCCESS', 20,
+ 'TEMPORARY_REDIRECT', 30,
+ 'PERMANENT_REDIRECT', 31,
+ 'TEMPORARY_FAILURE', 40,
+ 'SERVER_UNAVAILABLE', 41,
+ 'CGI_ERROR', 42,
+ 'PROXY_ERROR', 43,
+ 'SLOW_DOWN', 44,
+ 'PERMANENT_FAILURE', 50,
+ 'NOT_FOUND', 51,
+ 'GONE', 52,
+ 'PROXY_REQUEST_REFUSE', 53,
+ 'BAD_REQUEST', 59,
+ 'CLIENT_CERT_REQUIRED', 60,
+ 'CERT_NOT_AUTHORISED', 61,
+ 'CERT_NOT_VALID', 62
+);
+
+my $sitename = 'gmnifaq';
+my $siteintro = 'Welcome to gmnifaq!';
+
+my $dsn = "DBI:SQLite:dbname=data/data.sqlite";
+
+# enable UTF-8 mode for everything
+use utf8;
+binmode STDOUT, ':utf8';
+binmode STDERR, ':utf8';
+
+if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI')
+{
+ write_response('CGI_ERROR', 'CGI execution error', undef);
+}
+
+if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; }
+
+if ( !-f 'data/data.sqlite' ) { write_response('PERMANENT_FAILURE', 'Permanent failure', undef) };
+
+my @body = ();
+push @body, '# Welcome to '. $sitename;
+push @body, '';
+push @body, $siteintro;
+push @body, '';
+push @body, header();
+push @body, '';
+push @body, '## Meta';
+push @body, '';
+push @body, 'Search';
+push @body, '=> tags.pl Tags';
+push @body, 'View all';
+push @body, footer();
+
+write_response('SUCCESS', 'text/gemini', @body);
+
+exit;
+
+sub header
+{
+ my $dbh = DBI->connect($dsn, '', '', { RaiseError => 1 }) or die $DBI::errstr;
+
+ my $tagcount = $dbh->selectrow_array("SELECT count(id) from tags");
+ my $faqcount = $dbh->selectrow_array("SELECT count(id) from questions");
+ $dbh->disconnect();
+
+ return sprintf('We are currently serving %d FAQs categorized with %d tags!', $faqcount, $tagcount);
+}
+
+sub footer
+{
+ return ('', '=> https://git.sr.ht/~rwa/gmnifaq powered by gmnifaq');
+}
+
+sub write_response
+{
+ my ($returncode, $meta, @content) = @_;
+
+ if (!defined($RC{$returncode})) { die "Unknown response code!"; }
+
+ printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta);
+ foreach (@content)
+ {
+ print("$_\r\n");
+ }
+
+ exit;
+}
diff --git a/tags.pl b/tags.pl
new file mode 100755
index 0000000000000000000000000000000000000000..87405788d7c7506edf9d023c2b199d06fbf64030
--- /dev/null
+++ b/tags.pl
@@ -0,0 +1,105 @@
+#!/usr/bin/perl
+# Copyright René Wagner 2020
+# licenced under BSD 3-Clause licence
+# https://git.sr.ht/~rwa/gmni-perl-cgi-demo
+
+use strict;
+use DBI;
+#use URI::Encode qw(uri_encode uri_decode);
+# define return codes
+our %RC = (
+ 'INPUT', 10,
+ 'SENSITIVE_INPUT', 11,
+ 'SUCCESS', 20,
+ 'TEMPORARY_REDIRECT', 30,
+ 'PERMANENT_REDIRECT', 31,
+ 'TEMPORARY_FAILURE', 40,
+ 'SERVER_UNAVAILABLE', 41,
+ 'CGI_ERROR', 42,
+ 'PROXY_ERROR', 43,
+ 'SLOW_DOWN', 44,
+ 'PERMANENT_FAILURE', 50,
+ 'NOT_FOUND', 51,
+ 'GONE', 52,
+ 'PROXY_REQUEST_REFUSE', 53,
+ 'BAD_REQUEST', 59,
+ 'CLIENT_CERT_REQUIRED', 60,
+ 'CERT_NOT_AUTHORISED', 61,
+ 'CERT_NOT_VALID', 62
+);
+
+my $sitename;
+my $siteintro;
+
+my $dsn = "DBI:SQLite:dbname=data/data.sqlite";
+
+# enable UTF-8 mode for everything
+use utf8;
+binmode STDOUT, ':utf8';
+binmode STDERR, ':utf8';
+
+if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI')
+{
+ write_response('CGI_ERROR', 'CGI execution error', undef);
+}
+
+if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; }
+
+if ( !-f 'data/data.sqlite' ) { write_response('PERMANENT_FAILURE', 'Permanent failure', undef) };
+
+my @body = ();
+push @body, '# Welcome to '. $sitename;
+push @body, '';
+push @body, 'Select a tag to browse the questions associated with this tag.';
+push @body, '';
+push @body, '## Tags';
+push @body, '';
+push @body, tags();
+push @body, footer();
+
+write_response('SUCCESS', 'text/gemini', @body);
+
+exit;
+
+sub tags
+{
+ my $dbh = DBI->connect($dsn, '', '', { RaiseError => 1 }) or die $DBI::errstr;
+
+ my @tags;
+ my $stmt = $dbh->prepare('SELECT name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id GROUP BY t_id');
+ $stmt->execute();
+
+ my $rows = $stmt->fetchall_arrayref;
+ $dbh->disconnect();
+
+ if ( !scalar @$rows ) {
+ push @body, 'No tags found!';
+ }
+ else {
+ foreach (@$rows)
+ {
+ push @tags, sprintf("=> tags.pl?%s %s (%d entrys)", @$_[0], @$_[0], @$_[1]);
+ }
+ }
+ return @tags;
+}
+
+sub footer
+{
+ return ('', '=> index.pl [Home]', '=> https://git.sr.ht/~rwa/gmnifaq powered by gmnifaq');
+}
+
+sub write_response
+{
+ my ($returncode, $meta, @content) = @_;
+
+ if (!defined($RC{$returncode})) { die "Unknown response code!"; }
+
+ printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta);
+ foreach (@content)
+ {
+ print("$_\r\n");
+ }
+
+ exit;
+}