diff --git a/folders/sql/1_create_table.up.sql b/folders/sql/1_create_table.up.sql
index d518fc2de20f5899536ec2ceb55f1d57d5745475..14b47eb0183e39617b54e684dbd79286a80df5e5 100644
--- a/folders/sql/1_create_table.up.sql
+++ b/folders/sql/1_create_table.up.sql
@@ -85,8 +85,8 @@ BEGIN
   SELECT RAISE (ABORT, 'GENERAL folder is protected');
 END;
 
-INSERT INTO folders (name, description, system, shownew, owner)
-       VALUES ('GENERAL', 'Default general bulletin folder.', 1, 1, 'SYSTEM');
+INSERT INTO folders (name, description, system, shownew)
+       VALUES ('GENERAL', 'Default general bulletin folder.', 1, 1);
 
 CREATE TABLE owners (
   folder      VARCHAR(25)  REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE,
@@ -103,6 +103,8 @@ BEGIN
   UPDATE owners SET update_at=CURRENT_TIMESTAMP WHERE folder=NEW.folder AND owner=NEW.owner;
 END;
 
+INSERT INTO owners (folder, owner) VALUES ('GENERAL', 'SYSTEM');
+
 CREATE TABLE messages (
   id          INT          NOT NULL,
   folder      VARCHAR(25)  REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE,
@@ -149,14 +151,13 @@ CREATE TABLE access (
   PRIMARY KEY (login, folder)
 ) WITHOUT ROWID;
 
---- TODO: The following is incomplete:
---- User configs.
+--- User folder configs.
 CREATE TABLE config (
   login       VARCHAR(25) REFERENCES users(login) ON DELETE CASCADE ON UPDATE CASCADE,
   folder      VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE,
   always      INT     NOT NULL DEFAULT 0,
   alert       INT     NOT NULL DEFAULT 0,  --- 0=no, 1=brief, 2=readnew
-  always      INT     NOT NULL
+  PRIMARY KEY (login, folder)
 ) WITHOUT ROWID;
 
 --- System configs.
diff --git a/go.mod b/go.mod
index 09e451465015e300800b86f08c7f6e9d7ad6c86a..a8ad251f763411ca3a23312cc85555b32d21b293 100644
--- a/go.mod
+++ b/go.mod
@@ -15,23 +15,105 @@ require (
 )
 
 require (
+	cel.dev/expr v0.19.1 // indirect
+	dario.cat/mergo v1.0.1 // indirect
+	filippo.io/edwards25519 v1.1.0 // indirect
+	github.com/Masterminds/goutils v1.1.1 // indirect
+	github.com/Masterminds/semver/v3 v3.3.0 // indirect
+	github.com/Masterminds/sprig/v3 v3.3.0 // indirect
+	github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect
+	github.com/aarondl/opt v0.0.0-20230114172057-b91f370c41f0 // indirect
+	github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
+	github.com/coder/websocket v1.8.12 // indirect
+	github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
+	github.com/cubicdaiya/gonp v1.0.4 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/fatih/structtag v1.2.0 // indirect
+	github.com/fergusstrange/embedded-postgres v1.26.0 // indirect
+	github.com/fsnotify/fsnotify v1.6.0 // indirect
 	github.com/gdamore/encoding v1.0.1 // indirect
+	github.com/go-sql-driver/mysql v1.9.2 // indirect
+	github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
+	github.com/gofrs/uuid v4.4.0+incompatible // indirect
+	github.com/google/cel-go v0.24.1 // indirect
+	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
+	github.com/huandu/xstrings v1.5.0 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/jackc/pgpassfile v1.0.0 // indirect
+	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+	github.com/jackc/pgx/v5 v5.7.4 // indirect
+	github.com/jackc/puddle/v2 v2.2.2 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/knadh/koanf/maps v0.1.1 // indirect
+	github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect
+	github.com/knadh/koanf/providers/confmap v0.1.0 // indirect
+	github.com/knadh/koanf/providers/env v0.1.0 // indirect
+	github.com/knadh/koanf/providers/file v0.1.0 // indirect
+	github.com/knadh/koanf/v2 v2.1.0 // indirect
+	github.com/lib/pq v1.10.9 // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
+	github.com/mitchellh/copystructure v1.2.0 // indirect
+	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/ncruces/go-strftime v0.1.9 // indirect
+	github.com/pganalyze/pg_query_go/v6 v6.1.0 // indirect
+	github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb // indirect
+	github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect
+	github.com/pingcap/log v1.1.0 // indirect
+	github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 // indirect
+	github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
+	github.com/riza-io/grpc-go v0.2.0 // indirect
+	github.com/russross/blackfriday/v2 v2.1.0 // indirect
+	github.com/shopspring/decimal v1.4.0 // indirect
+	github.com/spf13/cast v1.7.0 // indirect
+	github.com/spf13/cobra v1.9.1 // indirect
+	github.com/spf13/pflag v1.0.6 // indirect
+	github.com/sqlc-dev/sqlc v1.29.0 // indirect
+	github.com/stephenafamo/bob v0.34.2 // indirect
+	github.com/stephenafamo/scan v0.6.2 // indirect
+	github.com/stephenafamo/sqlparser v0.0.0-20250408111851-b937299b5b7d // indirect
+	github.com/stoewer/go-strcase v1.2.0 // indirect
+	github.com/tetratelabs/wazero v1.9.0 // indirect
+	github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d // indirect
+	github.com/urfave/cli/v2 v2.23.7 // indirect
+	github.com/volatiletech/inflect v0.0.1 // indirect
+	github.com/volatiletech/strmangle v0.0.6 // indirect
+	github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 // indirect
+	github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect
+	github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
+	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	go.uber.org/zap v1.27.0 // indirect
+	golang.org/x/crypto v0.38.0 // indirect
 	golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
+	golang.org/x/mod v0.24.0 // indirect
+	golang.org/x/net v0.40.0 // indirect
+	golang.org/x/sync v0.14.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/term v0.32.0 // indirect
 	golang.org/x/text v0.25.0 // indirect
+	golang.org/x/tools v0.33.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
+	google.golang.org/grpc v1.71.1 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
+	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 	modernc.org/libc v1.65.2 // indirect
 	modernc.org/mathutil v1.7.1 // indirect
 	modernc.org/memory v1.10.0 // indirect
+	mvdan.cc/gofumpt v0.7.0 // indirect
+)
+
+tool (
+	github.com/sqlc-dev/sqlc/cmd/sqlc
+	github.com/stephenafamo/bob/gen/bobgen-sql
 )
diff --git a/go.sum b/go.sum
index 5fafbe6e01c71a8cc434e12b0802789ecbb39313..4b3c073d7d562fe1eec7bf9f669ddf8f8d89515f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,25 @@
+cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4=
+cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
+dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
+dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
+github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
+github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
+github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf h1:+edM69bH/X6JpYPmJYBRLanAMe1V5yRXYU3hHUovGcE=
+github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf/go.mod h1:FZqLhJSj2tg0ZN48GB1zvj00+ZYcHPqgsC7yzcgCq6k=
+github.com/aarondl/opt v0.0.0-20230114172057-b91f370c41f0 h1:vLrhbOWVPxtHao/QthU8pcpI4DbtSGnWgH7qIJf8F6k=
+github.com/aarondl/opt v0.0.0-20230114172057-b91f370c41f0/go.mod h1:l4/5NZtYd/SIohsFhaJQQe+sPOTG22furpZ5FvcYOzk=
 github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
 github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
+github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
+github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
 github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
 github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
@@ -10,19 +28,46 @@ github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI
 github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
 github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
 github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
+github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
+github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
+github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/cubicdaiya/gonp v1.0.4 h1:ky2uIAJh81WiLcGKBVD5R7KsM/36W6IqqTy6Bo6rGws=
+github.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYYACpOI0I=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 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/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
+github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
+github.com/fergusstrange/embedded-postgres v1.26.0 h1:mTgUBNST+6zro0TkIb9Fuo9Qg8mSU0ILus9jZKmFmJg=
+github.com/fergusstrange/embedded-postgres v1.26.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw=
+github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
 github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
 github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
 github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
 github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
 github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
 github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
+github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
+github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c=
+github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
+github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
 github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI=
+github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
 github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -32,8 +77,37 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
+github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
+github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
+github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
+github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
 github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
+github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
+github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
+github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w=
+github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY=
+github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU=
+github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU=
+github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg=
+github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ=
+github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c=
+github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA=
+github.com/knadh/koanf/v2 v2.1.0 h1:eh4QmHHBuU8BybfIJ8mB8K8gsGCD/AUQTdwGq/GzId8=
+github.com/knadh/koanf/v2 v2.1.0/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@@ -44,10 +118,28 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
 github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 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/pganalyze/pg_query_go/v6 v6.1.0 h1:jG5ZLhcVgL1FAw4C/0VNQaVmX1SUJx71wBGdtTtBvls=
+github.com/pganalyze/pg_query_go/v6 v6.1.0/go.mod h1:nvTHIuoud6e1SfrUaFwHqT0i4b5Nr+1rPWVds3B5+50=
+github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
+github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb h1:3pSi4EDG6hg0orE1ndHkXvX6Qdq2cZn8gAPir8ymKZk=
+github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg=
+github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 h1:tdMsjOqUR7YXHoBitzdebTvOjs/swniBTOLy5XiMtuE=
+github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86/go.mod h1:exzhVYca3WRtd6gclGNErRWb1qEgff3LYta0LvRmON4=
+github.com/pingcap/log v1.1.0 h1:ELiPxACz7vdo1qAvvaWJg1NrYFoY6gqAh/+Uo6aXdD8=
+github.com/pingcap/log v1.1.0/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4=
+github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 h1:W3rpAI3bubR6VWOcwxDIG0Gz9G5rl5b3SL116T0vBt0=
+github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0/go.mod h1:+8feuexTKcXHZF/dkDfvCwEyBAmgb4paFc3/WeYV2eE=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 h1:wSmWgpuccqS2IOfmYrbRiUgv+g37W5suLLLxwwniTSc=
+github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494/go.mod h1:yipyliwI08eQ6XwDm1fEwKPdF/xdbkiHtrU+1Hg+vc4=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rivo/tview v0.0.0-20250501113434-0c592cd31026 h1:ij8h8B3psk3LdMlqkfPTKIzeGzTaZLOiyplILMlxPAM=
@@ -56,20 +148,81 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
 github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/riza-io/grpc-go v0.2.0 h1:2HxQKFVE7VuYstcJ8zqpN84VnAoJ4dCL6YFhJewNcHQ=
+github.com/riza-io/grpc-go v0.2.0/go.mod h1:2bDvR9KkKC3KhtlSHfR3dAXjUMT86kg4UfWFyVGWqi8=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
+github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
+github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/sqlc-dev/sqlc v1.29.0 h1:HQctoD7y/i29Bao53qXO7CZ/BV9NcvpGpsJWvz9nKWs=
+github.com/sqlc-dev/sqlc v1.29.0/go.mod h1:BavmYw11px5AdPOjAVHmb9fctP5A8GTziC38wBF9tp0=
+github.com/stephenafamo/bob v0.34.2 h1:eXMmAE9YPKIyFKMXmI6wYI+dQxxuAnzulbmyHBctSGk=
+github.com/stephenafamo/bob v0.34.2/go.mod h1:EVqAHXIxKPppvrkVsy/+YiUyHDWueIh0srPENffFhNE=
+github.com/stephenafamo/scan v0.6.2 h1:mEjx1P1MuimqALCXfZEV8+KAiVcByrgngqKatgHag9I=
+github.com/stephenafamo/scan v0.6.2/go.mod h1:FhIUJ8pLNyex36xGFiazDJJ5Xry0UkAi+RkWRrEcRMg=
+github.com/stephenafamo/sqlparser v0.0.0-20250408111851-b937299b5b7d h1:VJwkSvMTq76O/CgKR9fvx4Dxf3cLx21ueBvVvFBriVY=
+github.com/stephenafamo/sqlparser v0.0.0-20250408111851-b937299b5b7d/go.mod h1:2ATW++wFz7Mvc/N+nUtQnU+9VIGAxrn8m9JCLDSWMsQ=
+github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
+github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
+github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
+github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU=
+github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=
+github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY=
+github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
 github.com/urfave/cli/v3 v3.3.2 h1:BYFVnhhZ8RqT38DxEYVFPPmGFTEf7tJwySTXsVRrS/o=
 github.com/urfave/cli/v3 v3.3.2/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
+github.com/volatiletech/inflect v0.0.1 h1:2a6FcMQyhmPZcLa+uet3VJ8gLn/9svWhJxJYwvE8KsU=
+github.com/volatiletech/inflect v0.0.1/go.mod h1:IBti31tG6phkHitLlr5j7shC5SOo//x0AjDzaJU1PLA=
+github.com/volatiletech/strmangle v0.0.6 h1:AdOYE3B2ygRDq4rXDij/MMwq6KVK/pWAYxpC7CLrkKQ=
+github.com/volatiletech/strmangle v0.0.6/go.mod h1:ycDvbDkjDvhC0NUU8w3fWwl5JEMTV56vTKXzR3GeR+0=
+github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 h1:mJdDDPblDfPe7z7go8Dvv1AJQDI3eQ/5xith3q2mFlo=
+github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07/go.mod h1:Ak17IJ037caFp4jpCw/iQQ7/W74Sqpb1YuKJU6HTKfM=
+github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4=
+github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY=
+github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
+github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
+github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
+github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
+golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
 golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
 golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -77,6 +230,7 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
 golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -85,6 +239,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
+golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -100,6 +256,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -132,6 +289,9 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
 golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
 golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
@@ -140,6 +300,29 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
 golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
 golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
+google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
+google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
+google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
+google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
@@ -166,3 +349,5 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
 modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
 modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
 modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
+mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo=
diff --git a/storage/connection.go b/storage/connection.go
new file mode 100644
index 0000000000000000000000000000000000000000..0b7a8c7572d27100fb555c1337ca08daa1849bd6
--- /dev/null
+++ b/storage/connection.go
@@ -0,0 +1,64 @@
+package storage
+
+import (
+	"embed"
+	"errors"
+	"os"
+	"path"
+
+	"github.com/adrg/xdg"
+	"github.com/golang-migrate/migrate/v4"
+	"github.com/golang-migrate/migrate/v4/source/iofs"
+	"github.com/jmoiron/sqlx"
+
+	// Included to connect to sqlite.
+	_ "github.com/golang-migrate/migrate/v4/database/sqlite"
+	_ "modernc.org/sqlite"
+)
+
+//go:embed migrations/*.sql
+var migrationsFS embed.FS
+
+// Store is the store for bulletin.
+type Store struct {
+	user string
+	db   *sqlx.DB
+}
+
+// Open opens the bulletin database.
+func Open(user string) (*Store, error) {
+	fdir := path.Join(xdg.DataHome, "BULLETIN")
+	err := os.MkdirAll(fdir, 0700)
+	if err != nil {
+		return nil, errors.New("bulletin directory problem")
+	}
+	fdb := path.Join(fdir, "bulletin.db")
+
+	// Run db migrations if needed.
+	migrations, err := iofs.New(migrationsFS, "migrations")
+	if err != nil {
+		return nil, err
+	}
+	m, err := migrate.NewWithSourceInstance("iofs", migrations,
+		"sqlite://"+fdb+"?_pragma=foreign_keys(1)")
+	if err != nil {
+		return nil, err
+	}
+	err = m.Up()
+	if err != nil && err != migrate.ErrNoChange {
+		return nil, err
+	}
+	m.Close()
+
+	store := &Store{user: user}
+	store.db, err = sqlx.Connect("sqlite", "file://"+fdb+"?_pragma=foreign_keys(1)")
+	if err != nil {
+		return nil, errors.New("bulletin database problem")
+	}
+	return store, nil
+}
+
+// Close closes the db backing the store.
+func (fstore *Store) Close() {
+	fstore.db.Close()
+}
diff --git a/storage/db.go b/storage/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..7f31d97184abf1b2cfc3d10cbb9770fa737a2c22
--- /dev/null
+++ b/storage/db.go
@@ -0,0 +1,31 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.29.0
+
+package storage
+
+import (
+	"context"
+	"database/sql"
+)
+
+type DBTX interface {
+	ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
+	PrepareContext(context.Context, string) (*sql.Stmt, error)
+	QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
+	QueryRowContext(context.Context, string, ...interface{}) *sql.Row
+}
+
+func New(db DBTX) *Queries {
+	return &Queries{db: db}
+}
+
+type Queries struct {
+	db DBTX
+}
+
+func (q *Queries) WithTx(tx *sql.Tx) *Queries {
+	return &Queries{
+		db: tx,
+	}
+}
diff --git a/storage/storage.go b/storage/doc.go
similarity index 100%
rename from storage/storage.go
rename to storage/doc.go
diff --git a/storage/folders.sql.go b/storage/folders.sql.go
new file mode 100644
index 0000000000000000000000000000000000000000..c2df48ff6ca3d20dd35fab7fa112f3d4a3cd70f5
--- /dev/null
+++ b/storage/folders.sql.go
@@ -0,0 +1,197 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.29.0
+// source: folders.sql
+
+package storage
+
+import (
+	"context"
+	"database/sql"
+)
+
+const createFolder = `-- name: CreateFolder :exec
+  INSERT INTO folders (
+  name, always, brief, description, notify, readnew, shownew, system,
+  expire, visibility
+) VALUES (
+  ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
+)
+`
+
+type CreateFolderParams struct {
+	Name        string
+	Always      int64
+	Brief       int64
+	Description string
+	Notify      int64
+	Readnew     int64
+	Shownew     int64
+	System      int64
+	Expire      int64
+	Visibility  int64
+}
+
+func (q *Queries) CreateFolder(ctx context.Context, arg CreateFolderParams) error {
+	_, err := q.db.ExecContext(ctx, createFolder,
+		arg.Name,
+		arg.Always,
+		arg.Brief,
+		arg.Description,
+		arg.Notify,
+		arg.Readnew,
+		arg.Shownew,
+		arg.System,
+		arg.Expire,
+		arg.Visibility,
+	)
+	return err
+}
+
+const deleteFolder = `-- name: DeleteFolder :exec
+DELETE FROM folders WHERE name=?
+`
+
+func (q *Queries) DeleteFolder(ctx context.Context, name string) error {
+	_, err := q.db.ExecContext(ctx, deleteFolder, name)
+	return err
+}
+
+const findFolderExact = `-- name: FindFolderExact :one
+SELECT name FROM folders where name = ?
+`
+
+func (q *Queries) FindFolderExact(ctx context.Context, name string) (string, error) {
+	row := q.db.QueryRowContext(ctx, findFolderExact, name)
+	err := row.Scan(&name)
+	return name, err
+}
+
+const findFolderPrefix = `-- name: FindFolderPrefix :one
+SELECT name FROM folders where name LIKE ?
+ORDER BY name
+LIMIT 1
+`
+
+func (q *Queries) FindFolderPrefix(ctx context.Context, name string) (string, error) {
+	row := q.db.QueryRowContext(ctx, findFolderPrefix, name)
+	err := row.Scan(&name)
+	return name, err
+}
+
+const getFolderExpire = `-- name: GetFolderExpire :one
+SELECT expire FROM folders WHERE name = ?
+`
+
+func (q *Queries) GetFolderExpire(ctx context.Context, name string) (int64, error) {
+	row := q.db.QueryRowContext(ctx, getFolderExpire, name)
+	var expire int64
+	err := row.Scan(&expire)
+	return expire, err
+}
+
+const isFolderAccess = `-- name: IsFolderAccess :one
+SELECT 1 FROM folders AS f LEFT JOIN owners AS c ON f.name = c.folder
+  WHERE f.name = ? AND (f.visibility = 0 OR c.OWNER = ?)
+`
+
+type IsFolderAccessParams struct {
+	Name  string
+	Owner sql.NullString
+}
+
+func (q *Queries) IsFolderAccess(ctx context.Context, arg IsFolderAccessParams) (int64, error) {
+	row := q.db.QueryRowContext(ctx, isFolderAccess, arg.Name, arg.Owner)
+	var column_1 int64
+	err := row.Scan(&column_1)
+	return column_1, err
+}
+
+const isFolderOwner = `-- name: IsFolderOwner :one
+SELECT 1 FROM folders AS f LEFT JOIN owners AS c ON f.name = c.folder
+  WHERE f.name = ? AND c.OWNER = ?
+`
+
+type IsFolderOwnerParams struct {
+	Name  string
+	Owner sql.NullString
+}
+
+func (q *Queries) IsFolderOwner(ctx context.Context, arg IsFolderOwnerParams) (int64, error) {
+	row := q.db.QueryRowContext(ctx, isFolderOwner, arg.Name, arg.Owner)
+	var column_1 int64
+	err := row.Scan(&column_1)
+	return column_1, err
+}
+
+const listFolder = `-- name: ListFolder :many
+SELECT f.name, count(m.id) as count, f.description
+FROM folders AS f LEFT JOIN messages AS m ON f.name = m.folder
+GROUP By f.name
+ORDER BY f.name
+`
+
+type ListFolderRow struct {
+	Name        string
+	Count       int64
+	Description string
+}
+
+func (q *Queries) ListFolder(ctx context.Context) ([]ListFolderRow, error) {
+	rows, err := q.db.QueryContext(ctx, listFolder)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	var items []ListFolderRow
+	for rows.Next() {
+		var i ListFolderRow
+		if err := rows.Scan(&i.Name, &i.Count, &i.Description); err != nil {
+			return nil, err
+		}
+		items = append(items, i)
+	}
+	if err := rows.Close(); err != nil {
+		return nil, err
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return items, nil
+}
+
+const listFolderForAdmin = `-- name: ListFolderForAdmin :many
+SELECT f.name, count(m.id) as count, f.description
+FROM folders AS f LEFT JOIN messages AS m ON f.name = m.folder
+GROUP By f.name
+ORDER BY f.name
+`
+
+type ListFolderForAdminRow struct {
+	Name        string
+	Count       int64
+	Description string
+}
+
+func (q *Queries) ListFolderForAdmin(ctx context.Context) ([]ListFolderForAdminRow, error) {
+	rows, err := q.db.QueryContext(ctx, listFolderForAdmin)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	var items []ListFolderForAdminRow
+	for rows.Next() {
+		var i ListFolderForAdminRow
+		if err := rows.Scan(&i.Name, &i.Count, &i.Description); err != nil {
+			return nil, err
+		}
+		items = append(items, i)
+	}
+	if err := rows.Close(); err != nil {
+		return nil, err
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return items, nil
+}
diff --git a/storage/generate.go b/storage/generate.go
new file mode 100644
index 0000000000000000000000000000000000000000..f590de0426faeadc03b4575ad08916b130020bc4
--- /dev/null
+++ b/storage/generate.go
@@ -0,0 +1,3 @@
+package storage
+
+//go:generate go tool github.com/sqlc-dev/sqlc/cmd/sqlc generate
diff --git a/storage/messages.sql.go b/storage/messages.sql.go
new file mode 100644
index 0000000000000000000000000000000000000000..62b82a477fd87173237a71c8cba1b754166eabb7
--- /dev/null
+++ b/storage/messages.sql.go
@@ -0,0 +1,124 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.29.0
+// source: messages.sql
+
+package storage
+
+import (
+	"context"
+	"database/sql"
+	"time"
+)
+
+const createMessage = `-- name: CreateMessage :exec
+INSERT INTO messages (
+  id, folder, author, subject, message, permanent, shutdown, expiration
+) VALUES (
+  (SELECT COALESCE(MAX(id), 0) + 1 FROM messages AS m WHERE m.folder = ?),
+  ?, ?, ?, ?, ?, ?, ?)
+`
+
+type CreateMessageParams struct {
+	Folder     sql.NullString
+	Folder_2   sql.NullString
+	Author     sql.NullString
+	Subject    string
+	Message    string
+	Permanent  int64
+	Shutdown   int64
+	Expiration time.Time
+}
+
+func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) error {
+	_, err := q.db.ExecContext(ctx, createMessage,
+		arg.Folder,
+		arg.Folder_2,
+		arg.Author,
+		arg.Subject,
+		arg.Message,
+		arg.Permanent,
+		arg.Shutdown,
+		arg.Expiration,
+	)
+	return err
+}
+
+const listMessages = `-- name: ListMessages :many
+SELECT id, folder, author, subject, message, expiration, create_at, update_at
+FROM messages
+WHERE folder = ?
+`
+
+type ListMessagesRow struct {
+	ID         int64
+	Folder     sql.NullString
+	Author     sql.NullString
+	Subject    string
+	Message    string
+	Expiration time.Time
+	CreateAt   time.Time
+	UpdateAt   time.Time
+}
+
+func (q *Queries) ListMessages(ctx context.Context, folder sql.NullString) ([]ListMessagesRow, error) {
+	rows, err := q.db.QueryContext(ctx, listMessages, folder)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	var items []ListMessagesRow
+	for rows.Next() {
+		var i ListMessagesRow
+		if err := rows.Scan(
+			&i.ID,
+			&i.Folder,
+			&i.Author,
+			&i.Subject,
+			&i.Message,
+			&i.Expiration,
+			&i.CreateAt,
+			&i.UpdateAt,
+		); err != nil {
+			return nil, err
+		}
+		items = append(items, i)
+	}
+	if err := rows.Close(); err != nil {
+		return nil, err
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return items, nil
+}
+
+const markMessage = `-- name: MarkMessage :exec
+INSERT INTO mark (login, folder, msgid) VALUES (?, ?, ?)
+`
+
+type MarkMessageParams struct {
+	Login  sql.NullString
+	Folder sql.NullString
+	Msgid  sql.NullInt64
+}
+
+func (q *Queries) MarkMessage(ctx context.Context, arg MarkMessageParams) error {
+	_, err := q.db.ExecContext(ctx, markMessage, arg.Login, arg.Folder, arg.Msgid)
+	return err
+}
+
+const setMessageSeen = `-- name: SetMessageSeen :exec
+INSERT INTO seen (login, folder, msgid) VALUES (?, ?, ?)
+`
+
+type SetMessageSeenParams struct {
+	Login  sql.NullString
+	Folder sql.NullString
+	Msgid  sql.NullInt64
+}
+
+func (q *Queries) SetMessageSeen(ctx context.Context, arg SetMessageSeenParams) error {
+	_, err := q.db.ExecContext(ctx, setMessageSeen, arg.Login, arg.Folder, arg.Msgid)
+	return err
+}
diff --git a/storage/migrations/1_create_table.down.sql b/storage/migrations/1_create_table.down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..8a1a47efb2c60d66a1e51323a8ca98c71013d3e2
--- /dev/null
+++ b/storage/migrations/1_create_table.down.sql
@@ -0,0 +1,21 @@
+--- Dropped in reverse order to deal with foreign keys.
+DROP TABLE mark;
+DROP TABLE read;
+
+DROP INDEX messages_idx_expiration;
+DROP INDEX messages_idx_shutdown;
+DROP TABLE messages;
+
+DROP TRIGGER co_owners_after_update_update_at;
+DROP TABLE co_owners;
+
+DROP TRIGGER folders_before_delete_protect;
+DROP TRIGGER folders_after_update_update_at;
+DROP TRIGGER folders_before_update_validate;
+DROP TRIGGER folders_before_insert_validate;
+DROP TABLE folders;
+
+DROP TRIGGER users_before_delete_protect;
+DROP TRIGGER users_before_update_protect;
+DROP TRIGGER users_after_update_update_at;
+DROP TABLE users;
diff --git a/storage/migrations/1_create_table.up.sql b/storage/migrations/1_create_table.up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..0aafda908e8368ad6b23e759b88c2e0078d81a78
--- /dev/null
+++ b/storage/migrations/1_create_table.up.sql
@@ -0,0 +1,161 @@
+CREATE TABLE users (
+  login       VARCHAR(12)  NOT NULL PRIMARY KEY,
+  name        VARCHAR(53)  NOT NULL,
+  admin       INT          DEFAULT 0 NOT NULL,
+  moderator   INT          DEFAULT 0 NOT NULL,
+  alert       INT          NOT NULL DEFAULT 0,  --- 0=no, 1=brief, 2=readnew
+  disabled    INT          DEFAULT 0 NOT NULL,
+  last_login  TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
+  create_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
+  update_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL
+) WITHOUT ROWID;
+
+CREATE TRIGGER users_after_update_update_at
+  AFTER UPDATE ON users FOR EACH ROW
+  WHEN NEW.update_at = OLD.update_at    --- avoid infinite loop
+BEGIN
+  UPDATE users SET update_at=CURRENT_TIMESTAMP WHERE login=NEW.login;
+END;
+
+CREATE TRIGGER users_before_update_protect
+  AFTER UPDATE ON users FOR EACH ROW
+  WHEN OLD.login = 'SYSTEM' AND (NEW.login != OLD.login OR NEW.admin != 1)
+BEGIN
+  SELECT RAISE (ABORT, 'SYSTEM user is protected');
+END;
+
+CREATE TRIGGER users_before_delete_protect
+  BEFORE DELETE on users FOR EACH ROW
+  WHEN OLD.login = 'SYSTEM'
+BEGIN
+  SELECT RAISE (ABORT, 'SYSTEM user is protected');
+END;
+
+CREATE TABLE folders (
+  name        VARCHAR(25)  NOT NULL PRIMARY KEY,
+  always      INT          DEFAULT 0 NOT NULL,
+  brief       INT          DEFAULT 0 NOT NULL,
+  description VARCHAR(53)  DEFAULT 0 NOT NULL,
+  notify      INT          DEFAULT 0 NOT NULL,
+  readnew     INT          DEFAULT 0 NOT NULL,
+  shownew     INT          DEFAULT 0 NOT NULL,
+  system      INT          DEFAULT 0 NOT NULL,
+  expire      INT          DEFAULT 14 NOT NULL,
+  --- public=0, semiprivate=1, private=2
+  visibility  INT          DEFAULT 0 NOT NULL,
+  create_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
+  update_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL
+) WITHOUT ROWID;
+
+CREATE TRIGGER folders_before_insert_validate
+  BEFORE INSERT on folders
+BEGIN
+  SELECT
+    CASE
+      WHEN NEW.name != UPPER(NEW.name) OR NEW.name GLOB '*[^A-Z0-9_-]*' THEN
+        RAISE (ABORT, 'Invalid folder name')
+    END;
+END;
+
+CREATE TRIGGER folders_before_update_validate
+  BEFORE UPDATE on folders
+BEGIN
+  SELECT
+    CASE
+      WHEN NEW.name != UPPER(NEW.name) OR NEW.name GLOB '*[^A-Z0-9_-]*' THEN
+        RAISE (ABORT, 'Invalid folder name')
+      WHEN OLD.name = 'GENERAL' AND OLD.name != NEW.name THEN
+        RAISE (ABORT, 'GENERAL folder is protected')
+    END;
+END;
+
+CREATE TRIGGER folders_after_update_update_at
+  AFTER UPDATE ON folders FOR EACH ROW
+    WHEN NEW.update_at = OLD.update_at    --- avoid infinite loop
+BEGIN
+  UPDATE folders SET update_at=CURRENT_TIMESTAMP WHERE name=NEW.name;
+END;
+
+CREATE TRIGGER folders_before_delete_protect
+  BEFORE DELETE on folders FOR EACH ROW
+  WHEN OLD.name = 'GENERAL'
+BEGIN
+  SELECT RAISE (ABORT, 'GENERAL folder is protected');
+END;
+
+CREATE TABLE owners (
+  folder      VARCHAR(25)  REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE,
+  owner       VARCHAR(25)  REFERENCES users(login) ON UPDATE CASCADE,
+  create_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
+  update_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
+  PRIMARY KEY (folder, owner)
+) WITHOUT ROWID;
+
+CREATE TRIGGER owners_after_update_update_at
+  AFTER UPDATE ON owners FOR EACH ROW
+    WHEN NEW.update_at = OLD.update_at    --- avoid infinite loop
+BEGIN
+  UPDATE owners SET update_at=CURRENT_TIMESTAMP WHERE folder=NEW.folder AND owner=NEW.owner;
+END;
+
+CREATE TABLE messages (
+  id          INT          NOT NULL,
+  folder      VARCHAR(25)  REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE,
+  author      VARCHAR(25)  REFERENCES users(login) ON UPDATE CASCADE,
+  subject     VARCHAR(53)  NOT NULL,
+  message     TEXT         NOT NULL,
+  permanent   INT          DEFAULT 0 NOT NULL,
+  shutdown    INT          DEFAULT 0 NOT NULL,
+  expiration  TIMESTAMP    NOT NULL,
+  create_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
+  update_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
+  PRIMARY KEY (id, folder)
+) WITHOUT ROWID;
+CREATE INDEX messages_idx_shutdown ON messages(shutdown);
+CREATE INDEX messages_idx_expiration ON messages(expiration);
+
+CREATE TABLE seen (
+  login       VARCHAR(25) REFERENCES users(login) ON DELETE CASCADE ON UPDATE CASCADE,
+  folder      VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE,
+  msgid       INT,
+  PRIMARY KEY (folder, login, msgid),
+  CONSTRAINT read_fk_id_folder
+    FOREIGN KEY (msgid, folder)
+    REFERENCES messages(id, folder)
+    ON DELETE CASCADE
+    ON UPDATE CASCADE
+) WITHOUT ROWID;
+
+CREATE TABLE mark (
+  login       VARCHAR(25) REFERENCES users(login) ON DELETE CASCADE ON UPDATE CASCADE,
+  folder      VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE,
+  msgid       INT,
+  PRIMARY KEY (folder, login, msgid),
+  CONSTRAINT mark_fk_id_folder
+    FOREIGN KEY (msgid, folder)
+    REFERENCES messages(id, folder)
+    ON DELETE CASCADE
+    ON UPDATE CASCADE
+) WITHOUT ROWID;
+
+CREATE TABLE access (
+  login       VARCHAR(25) REFERENCES users(login) ON DELETE CASCADE ON UPDATE CASCADE,
+  folder      VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE,
+  PRIMARY KEY (login, folder)
+) WITHOUT ROWID;
+
+--- User folder configs.
+CREATE TABLE config (
+  login       VARCHAR(25) REFERENCES users(login) ON DELETE CASCADE ON UPDATE CASCADE,
+  folder      VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE,
+  always      INT     NOT NULL DEFAULT 0,
+  alert       INT     NOT NULL DEFAULT 0,  --- 0=no, 1=brief, 2=readnew
+  PRIMARY KEY (login, folder)
+) WITHOUT ROWID;
+
+--- System configs.
+CREATE TABLE system (
+  name            VARCHAR(12)  NOT NULL PRIMARY KEY,
+  default_expire  INT          NOT NULL DEFAULT -1,
+  expire_limit    INT          NOT NULL DEFAULT -1
+);
diff --git a/storage/models.go b/storage/models.go
new file mode 100644
index 0000000000000000000000000000000000000000..13279efc60e582c3144ff5daf49f14eaa21a9da1
--- /dev/null
+++ b/storage/models.go
@@ -0,0 +1,87 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.29.0
+
+package storage
+
+import (
+	"database/sql"
+	"time"
+)
+
+type Access struct {
+	Login  sql.NullString
+	Folder sql.NullString
+}
+
+type Config struct {
+	Login  sql.NullString
+	Folder sql.NullString
+	Always int64
+	Alert  int64
+}
+
+type Folder struct {
+	Name        string
+	Always      int64
+	Brief       int64
+	Description string
+	Notify      int64
+	Readnew     int64
+	Shownew     int64
+	System      int64
+	Expire      int64
+	Visibility  int64
+	CreateAt    time.Time
+	UpdateAt    time.Time
+}
+
+type Mark struct {
+	Login  sql.NullString
+	Folder sql.NullString
+	Msgid  sql.NullInt64
+}
+
+type Message struct {
+	ID         int64
+	Folder     sql.NullString
+	Author     sql.NullString
+	Subject    string
+	Message    string
+	Permanent  int64
+	Shutdown   int64
+	Expiration time.Time
+	CreateAt   time.Time
+	UpdateAt   time.Time
+}
+
+type Owner struct {
+	Folder   sql.NullString
+	Owner    sql.NullString
+	CreateAt time.Time
+	UpdateAt time.Time
+}
+
+type Seen struct {
+	Login  sql.NullString
+	Folder sql.NullString
+	Msgid  sql.NullInt64
+}
+
+type System struct {
+	Name          string
+	DefaultExpire int64
+	ExpireLimit   int64
+}
+
+type User struct {
+	Login     string
+	Name      string
+	Admin     int64
+	Moderator int64
+	Alert     int64
+	Disabled  int64
+	LastLogin time.Time
+	CreateAt  time.Time
+	UpdateAt  time.Time
+}
diff --git a/storage/queries/folders.sql b/storage/queries/folders.sql
new file mode 100644
index 0000000000000000000000000000000000000000..70d6749a36b5f527a87c11e97e6a3d0afa3975c7
--- /dev/null
+++ b/storage/queries/folders.sql
@@ -0,0 +1,41 @@
+-- name: CreateFolder :exec
+  INSERT INTO folders (
+  name, always, brief, description, notify, readnew, shownew, system,
+  expire, visibility
+) VALUES (
+  ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
+);
+
+-- name: ListFolderForAdmin :many
+SELECT f.name, count(m.id) as count, f.description
+FROM folders AS f LEFT JOIN messages AS m ON f.name = m.folder
+GROUP By f.name
+ORDER BY f.name;
+
+-- name: ListFolder :many
+SELECT f.name, count(m.id) as count, f.description
+FROM folders AS f LEFT JOIN messages AS m ON f.name = m.folder
+GROUP By f.name
+ORDER BY f.name;
+
+-- name: FindFolderExact :one
+SELECT name FROM folders where name = ?;
+
+-- name: FindFolderPrefix :one
+SELECT name FROM folders where name LIKE ?
+ORDER BY name
+LIMIT 1;
+
+-- name: IsFolderAccess :one
+SELECT 1 FROM folders AS f LEFT JOIN owners AS c ON f.name = c.folder
+  WHERE f.name = ? AND (f.visibility = 0 OR c.OWNER = ?);
+
+-- name: IsFolderOwner :one
+SELECT 1 FROM folders AS f LEFT JOIN owners AS c ON f.name = c.folder
+  WHERE f.name = ? AND c.OWNER = ?;
+
+-- name: DeleteFolder :exec
+DELETE FROM folders WHERE name=?;
+
+-- name: GetFolderExpire :one
+SELECT expire FROM folders WHERE name = ?;
diff --git a/storage/queries/messages.sql b/storage/queries/messages.sql
new file mode 100644
index 0000000000000000000000000000000000000000..2f87c6835da581b1693f6d4551ee30cb026058ec
--- /dev/null
+++ b/storage/queries/messages.sql
@@ -0,0 +1,17 @@
+-- name: CreateMessage :exec
+INSERT INTO messages (
+  id, folder, author, subject, message, permanent, shutdown, expiration
+) VALUES (
+  (SELECT COALESCE(MAX(id), 0) + 1 FROM messages AS m WHERE m.folder = ?),
+  ?, ?, ?, ?, ?, ?, ?);
+
+-- name: SetMessageSeen :exec
+INSERT INTO seen (login, folder, msgid) VALUES (?, ?, ?);
+
+-- name: MarkMessage :exec
+INSERT INTO mark (login, folder, msgid) VALUES (?, ?, ?);
+
+-- name: ListMessages :many
+SELECT id, folder, author, subject, message, expiration, create_at, update_at
+FROM messages
+WHERE folder = ?;
diff --git a/storage/queries/seed.sql b/storage/queries/seed.sql
new file mode 100644
index 0000000000000000000000000000000000000000..a1f0a31291cf32adfcc6201a2027a60a812e2d51
--- /dev/null
+++ b/storage/queries/seed.sql
@@ -0,0 +1,10 @@
+-- name: SeedUserSystem :exec
+  INSERT INTO users (login, name, admin)
+         VALUES ('SYSTEM', 'System User', 1);
+
+-- name: SeedFolderGeneral :exec
+  INSERT INTO folders (name, description, system, shownew)
+         VALUES ('GENERAL', 'Default general bulletin folder.', 1, 1);
+
+-- name: SeedGeneralOwner :exec
+  INSERT INTO owners (folder, owner) VALUES ('GENERAL', 'SYSTEM');
diff --git a/storage/queries/users.sql b/storage/queries/users.sql
new file mode 100644
index 0000000000000000000000000000000000000000..50ca0c67c5fcfd2eae53ba2e16bf2a3b913a0912
--- /dev/null
+++ b/storage/queries/users.sql
@@ -0,0 +1,8 @@
+-- name: GetUser :one
+SELECT login, name, admin, disabled FROM users WHERE login = ?;
+
+-- name: AddUser :exec
+INSERT INTO users (login, name, admin) VALUES (?, ?, ?);
+
+-- name: IsUserAdmin :one
+SELECT admin FROM users WHERE login = ?;
diff --git a/storage/seed.sql.go b/storage/seed.sql.go
new file mode 100644
index 0000000000000000000000000000000000000000..a52da4f05098e35119317201faa66f0bcc238b8f
--- /dev/null
+++ b/storage/seed.sql.go
@@ -0,0 +1,39 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.29.0
+// source: seed.sql
+
+package storage
+
+import (
+	"context"
+)
+
+const seedFolderGeneral = `-- name: SeedFolderGeneral :exec
+  INSERT INTO folders (name, description, system, shownew)
+         VALUES ('GENERAL', 'Default general bulletin folder.', 1, 1)
+`
+
+func (q *Queries) SeedFolderGeneral(ctx context.Context) error {
+	_, err := q.db.ExecContext(ctx, seedFolderGeneral)
+	return err
+}
+
+const seedGeneralOwner = `-- name: SeedGeneralOwner :exec
+  INSERT INTO owners (folder, owner) VALUES ('GENERAL', 'SYSTEM')
+`
+
+func (q *Queries) SeedGeneralOwner(ctx context.Context) error {
+	_, err := q.db.ExecContext(ctx, seedGeneralOwner)
+	return err
+}
+
+const seedUserSystem = `-- name: SeedUserSystem :exec
+  INSERT INTO users (login, name, admin)
+         VALUES ('SYSTEM', 'System User', 1)
+`
+
+func (q *Queries) SeedUserSystem(ctx context.Context) error {
+	_, err := q.db.ExecContext(ctx, seedUserSystem)
+	return err
+}
diff --git a/storage/sqlc.yaml b/storage/sqlc.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a870855dc2f14b03afc9739ad1c4ae4d5b212217
--- /dev/null
+++ b/storage/sqlc.yaml
@@ -0,0 +1,10 @@
+---
+version: "2"
+sql:
+  - engine: "sqlite"
+    queries: "queries/"
+    schema: "migrations/"
+    gen:
+      go:
+        package: "storage"
+        out: "."
diff --git a/storage/users.sql.go b/storage/users.sql.go
new file mode 100644
index 0000000000000000000000000000000000000000..d51be82cab851d1637f03dfbad4f893a2f875a4d
--- /dev/null
+++ b/storage/users.sql.go
@@ -0,0 +1,59 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.29.0
+// source: users.sql
+
+package storage
+
+import (
+	"context"
+)
+
+const addUser = `-- name: AddUser :exec
+INSERT INTO users (login, name, admin) VALUES (?, ?, ?)
+`
+
+type AddUserParams struct {
+	Login string
+	Name  string
+	Admin int64
+}
+
+func (q *Queries) AddUser(ctx context.Context, arg AddUserParams) error {
+	_, err := q.db.ExecContext(ctx, addUser, arg.Login, arg.Name, arg.Admin)
+	return err
+}
+
+const getUser = `-- name: GetUser :one
+SELECT login, name, admin, disabled FROM users WHERE login = ?
+`
+
+type GetUserRow struct {
+	Login    string
+	Name     string
+	Admin    int64
+	Disabled int64
+}
+
+func (q *Queries) GetUser(ctx context.Context, login string) (GetUserRow, error) {
+	row := q.db.QueryRowContext(ctx, getUser, login)
+	var i GetUserRow
+	err := row.Scan(
+		&i.Login,
+		&i.Name,
+		&i.Admin,
+		&i.Disabled,
+	)
+	return i, err
+}
+
+const isUserAdmin = `-- name: IsUserAdmin :one
+SELECT admin FROM users WHERE login = ?
+`
+
+func (q *Queries) IsUserAdmin(ctx context.Context, login string) (int64, error) {
+	row := q.db.QueryRowContext(ctx, isUserAdmin, login)
+	var admin int64
+	err := row.Scan(&admin)
+	return admin, err
+}