diff --git a/README.md b/README.md
index 207a947..eeb3476 100644
--- a/README.md
+++ b/README.md
@@ -9,15 +9,17 @@ Most session libraries are highly opinionated and hard-wired to work with `net/h
 ## Features
 1. Framework/network library agnostic.
 2. Simple API and with support for primitive data types. Complex types can be stored using own encoding/decoding.
-3. Bundled Redis and in-memory stores.
+3. Pre-built redis/postgres/in-memory stores that can be separately installed.
 4. Multiple session instances with custom handlers and different backend stores.
 
 ## Installation
 Install `simplesessions` and all [available stores](/stores).
 
-```
+```shell
 go get -u github.com/vividvilla/simplesessions
-go get -u github.com/vividvilla/simplesessions/stores/...
+
+# Install the requrired store: memory|goredis|redis|postgres
+go get -u github.com/vividvilla/simplesessions/stores/goredis
 ```
 
 # Stores
diff --git a/go.work b/go.work
index c30543b..f692f89 100644
--- a/go.work
+++ b/go.work
@@ -1,4 +1,4 @@
-go 1.21
+go 1.14
 
 use (
 	.
@@ -7,4 +7,5 @@ use (
 	./stores/memory
 	./stores/redis
 	./stores/securecookie
+	./stores/postgres
 )
diff --git a/stores/postgres/go.mod b/stores/postgres/go.mod
new file mode 100644
index 0000000..002772c
--- /dev/null
+++ b/stores/postgres/go.mod
@@ -0,0 +1,5 @@
+module github.com/vividvilla/simplesessions/stores/postgres
+
+go 1.14
+
+require github.com/lib/pq v1.10.9
diff --git a/stores/postgres/postgres.go b/stores/postgres/postgres.go
new file mode 100644
index 0000000..5d05c03
--- /dev/null
+++ b/stores/postgres/postgres.go
@@ -0,0 +1,394 @@
+package postgres
+
+/*
+CREATE TABLE sessions (
+    id TEXT NOT NULL PRIMARY KEY,
+    data jsonb DEFAULT '{}'::jsonb NOT NULL,
+    created_at timestamp without time zone DEFAULT now() NOT NULL
+);
+CREATE INDEX idx_sessions ON sessions (id, created_at);
+*/
+
+import (
+	"crypto/rand"
+	"database/sql"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"sync"
+	"time"
+	"unicode"
+
+	_ "github.com/lib/pq"
+)
+
+var (
+	// Error codes for store errors. This should match the codes
+	// defined in the /simplesessions package exactly.
+	ErrInvalidSession = &Err{code: 1, msg: "invalid session"}
+	ErrFieldNotFound  = &Err{code: 2, msg: "field not found"}
+	ErrAssertType     = &Err{code: 3, msg: "assertion failed"}
+	ErrNil            = &Err{code: 4, msg: "nil returned"}
+)
+
+type Err struct {
+	code int
+	msg  string
+}
+
+func (e *Err) Error() string {
+	return e.msg
+}
+
+func (e *Err) Code() int {
+	return e.code
+}
+
+type queries struct {
+	create *sql.Stmt
+	get    *sql.Stmt
+	getAll *sql.Stmt
+	update *sql.Stmt
+	delete *sql.Stmt
+	clear  *sql.Stmt
+}
+
+// Store represents redis session store for simple sessions.
+// Each session is stored as redis hashmap.
+type Store struct {
+	db  *sql.DB
+	opt Opt
+	q   *queries
+
+	commitID string
+	tx       *sql.Tx
+	stmt     *sql.Stmt
+	sync.Mutex
+}
+
+type Opt struct {
+	Table string        `json:"table"`
+	TTL   time.Duration `json:"ttl"`
+
+	// Delete expired (TTL) rows from the table at this interval.
+	// This runs concurrently on a separate goroutine.
+	CleanInterval time.Duration `json:"clean_interval"`
+}
+
+const (
+	sessionIDLen = 32
+)
+
+// New creates a new Postgres store instance.
+func New(opt Opt, db *sql.DB) (*Store, error) {
+	if opt.Table == "" {
+		opt.Table = "sessions"
+	}
+	if opt.TTL.Seconds() < 1 {
+		opt.TTL = time.Hour * 24
+	}
+	if opt.CleanInterval.Seconds() < 1 {
+		opt.CleanInterval = time.Hour * 1
+	}
+
+	st := &Store{
+		db:  db,
+		opt: opt,
+	}
+
+	// Prepare and keep the queries.
+	q, err := st.prepareQueries()
+	if err != nil {
+		return nil, err
+	}
+	st.q = q
+
+	return st, nil
+}
+
+// Create creates a new session and returns the ID.
+func (s *Store) Create() (string, error) {
+	id, err := generateID(sessionIDLen)
+	if err != nil {
+		return "", err
+	}
+
+	if _, err := s.q.create.Exec(id); err != nil {
+		return "", err
+	}
+	return id, nil
+}
+
+// Get returns a single session field's value.
+func (s *Store) Get(id, key string) (interface{}, error) {
+	if !validateID(id) {
+		return nil, ErrInvalidSession
+	}
+
+	// Scan the whole JSON map out so that it can be unmarshalled,
+	// preserving the types.
+	var b []byte
+	if err := s.q.get.QueryRow(id, s.opt.TTL.Seconds()).Scan(&b); err != nil {
+		return nil, err
+	}
+
+	var mp map[string]interface{}
+	if err := json.Unmarshal(b, &mp); err != nil {
+		return nil, err
+	}
+
+	v, ok := mp[key]
+	if !ok {
+		return nil, ErrFieldNotFound
+	}
+
+	return v, nil
+}
+
+// GetMulti gets a map for values for multiple keys. If a key doesn't exist, it returns ErrFieldNotFound.
+func (s *Store) GetMulti(id string, keys ...string) (map[string]interface{}, error) {
+	if !validateID(id) {
+		return nil, ErrInvalidSession
+	}
+
+	vals, err := s.GetAll(id)
+	if err != nil {
+		return nil, err
+	}
+
+	out := make(map[string]interface{}, len(keys))
+	for _, k := range keys {
+		v, ok := vals[k]
+		if !ok {
+			return nil, ErrFieldNotFound
+		}
+		out[k] = v
+	}
+
+	return out, err
+}
+
+// GetAll returns the map of all keys in the session.
+func (s *Store) GetAll(id string, keys ...string) (map[string]interface{}, error) {
+	if !validateID(id) {
+		return nil, ErrInvalidSession
+	}
+
+	var b []byte
+	err := s.q.getAll.QueryRow(id, s.opt.TTL.Seconds()).Scan(&b)
+	if err != nil {
+		return nil, err
+	}
+
+	out := make(map[string]interface{})
+	if err := json.Unmarshal(b, &out); err != nil {
+		return nil, err
+	}
+
+	return out, err
+}
+
+// Set sets a value to given session but is stored only on commit.
+func (s *Store) Set(id, key string, val interface{}) (err error) {
+	if !validateID(id) {
+		return ErrInvalidSession
+	}
+
+	b, err := json.Marshal(map[string]interface{}{key: val})
+	if err != nil {
+		return err
+	}
+
+	s.Lock()
+	defer func() {
+		if err == nil {
+			s.Unlock()
+			return
+		}
+
+		if s.tx != nil {
+			s.tx.Rollback()
+			s.tx = nil
+		}
+		s.stmt = nil
+
+		s.Unlock()
+	}()
+
+	// If a transaction isn't set, set it.
+	if s.tx == nil {
+		tx, err := s.db.Begin()
+		if err != nil {
+			return err
+		}
+
+		// Prepare the statement for executing SQL commands
+		s.tx = tx
+		s.stmt = tx.Stmt(s.q.update)
+	}
+
+	// Execute the query in the batch to be committed later.
+	res, err := s.stmt.Exec(id, json.RawMessage(b))
+	if err != nil {
+		return err
+	}
+	num, err := res.RowsAffected()
+	if err != nil {
+		return err
+	}
+
+	// No row was updated. The session didn't exist.
+	if num == 0 {
+		return ErrInvalidSession
+	}
+
+	s.commitID = id
+	return err
+}
+
+// Commit sets all set values
+func (s *Store) Commit(id string) error {
+	if !validateID(id) {
+		return ErrInvalidSession
+	}
+
+	s.Lock()
+	if s.commitID != id {
+		s.Unlock()
+		return ErrInvalidSession
+	}
+
+	defer func() {
+		if s.stmt != nil {
+			s.stmt.Close()
+		}
+		s.tx = nil
+		s.stmt = nil
+		s.Unlock()
+	}()
+
+	if s.tx == nil {
+		return errors.New("nothing to commit")
+	}
+	if s.commitID != id {
+		return ErrInvalidSession
+	}
+
+	return s.tx.Commit()
+}
+
+// Delete deletes a key from redis session hashmap.
+func (s *Store) Delete(id string, key string) error {
+	if !validateID(id) {
+		return ErrInvalidSession
+	}
+
+	res, err := s.q.delete.Exec(id, key)
+	if err != nil {
+		return err
+	}
+
+	num, err := res.RowsAffected()
+	if err != nil {
+		return err
+	}
+
+	// No row was updated. The session didn't exist.
+	if num == 0 {
+		return ErrInvalidSession
+	}
+
+	return nil
+}
+
+// Clear clears session in redis.
+func (s *Store) Clear(id string) error {
+	if !validateID(id) {
+		return ErrInvalidSession
+	}
+
+	res, err := s.db.Exec(fmt.Sprintf("UPDATE %s SET data = '{}'::JSONB WHERE id=$1", s.opt.Table), id)
+	if err != nil {
+		return err
+	}
+
+	num, err := res.RowsAffected()
+	if err != nil {
+		return err
+	}
+
+	// No row was updated. The session didn't exist.
+	if num == 0 {
+		return ErrInvalidSession
+	}
+
+	return nil
+}
+
+func (s *Store) prepareQueries() (*queries, error) {
+	var (
+		q   = &queries{}
+		err error
+	)
+
+	q.create, err = s.db.Prepare(fmt.Sprintf("INSERT INTO %s (id, data) VALUES($1, '{}'::JSONB)", s.opt.Table))
+	if err != nil {
+		return nil, err
+	}
+
+	q.get, err = s.db.Prepare(fmt.Sprintf("SELECT data FROM %s WHERE id=$1 AND created_at >= NOW() - INTERVAL '1 second' * $2", s.opt.Table))
+	if err != nil {
+		return nil, err
+	}
+
+	q.getAll, err = s.db.Prepare(fmt.Sprintf("SELECT data FROM %s WHERE id=$1 AND created_at >= NOW() - INTERVAL '1 second' * $2", s.opt.Table))
+	if err != nil {
+		return nil, err
+	}
+
+	q.update, err = s.db.Prepare(fmt.Sprintf("UPDATE %s SET data = data || $2::JSONB WHERE id = $1", s.opt.Table))
+	if err != nil {
+		return nil, err
+	}
+
+	q.delete, err = s.db.Prepare(fmt.Sprintf("UPDATE %s SET data = data - $2 WHERE id=$1", s.opt.Table))
+	if err != nil {
+		return nil, err
+	}
+
+	q.clear, err = s.db.Prepare(fmt.Sprintf("UPDATE %s SET data = '{}'::JSONB WHERE id=$1", s.opt.Table))
+	if err != nil {
+		return nil, err
+	}
+
+	return q, err
+}
+
+func validateID(id string) bool {
+	if len(id) != sessionIDLen {
+		return false
+	}
+
+	for _, r := range id {
+		if !unicode.IsDigit(r) && !unicode.IsLetter(r) {
+			return false
+		}
+	}
+
+	return true
+}
+
+// generateID generates a random alpha-num session ID.
+func generateID(n int) (string, error) {
+	const dict = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+	bytes := make([]byte, n)
+	if _, err := rand.Read(bytes); err != nil {
+		return "", err
+	}
+
+	for k, v := range bytes {
+		bytes[k] = dict[v%byte(len(dict))]
+	}
+
+	return string(bytes), nil
+}
diff --git a/stores/postgres/postgres_test.go b/stores/postgres/postgres_test.go
new file mode 100644
index 0000000..0d6defb
--- /dev/null
+++ b/stores/postgres/postgres_test.go
@@ -0,0 +1,121 @@
+package postgres
+
+// For this test to run, set env vars: PG_HOST, PG_PORT, PG_USER, PG_PASSWORD, PG_DB.
+
+import (
+	"database/sql"
+	"fmt"
+	"log"
+	"os"
+	"testing"
+
+	_ "github.com/lib/pq"
+	"github.com/stretchr/testify/assert"
+)
+
+var (
+	st        *Store
+	randID, _ = generateID(sessionIDLen)
+)
+
+func init() {
+	if os.Getenv("PG_HOST") == "" {
+		fmt.Println("WARNING: Skiping DB test as database config isn't set in env vars.")
+		os.Exit(0)
+	}
+
+	p := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
+		os.Getenv("PG_HOST"), os.Getenv("PG_PORT"), os.Getenv("PG_USER"), os.Getenv("PG_PASSWORD"), os.Getenv("PG_DB"))
+	db, err := sql.Open("postgres", p)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	if err := db.Ping(); err != nil {
+		log.Fatal(err)
+	}
+
+	st, err = New(Opt{}, db)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	for n := 0; n < 5; n++ {
+		id, err := st.Create()
+		assert.NoError(t, err)
+		assert.NotEmpty(t, id)
+	}
+}
+
+func TestSet(t *testing.T) {
+	assert.NotEmpty(t, randID)
+
+	id, err := st.Create()
+	assert.NoError(t, err)
+	assert.NotEmpty(t, id)
+
+	assert.NoError(t, st.Set(id, "num", 123))
+	assert.NoError(t, st.Set(id, "str", "hello 123"))
+	assert.NoError(t, st.Set(id, "bool", true))
+
+	// Commit invalid session.
+	assert.Error(t, st.Commit(randID), ErrInvalidSession)
+
+	// Commit valid session.
+	assert.NoError(t, st.Commit(id))
+
+	// Commit without setting.
+	assert.Error(t, st.Commit(id))
+	assert.Error(t, st.Commit(randID))
+
+	// Get different types.
+	v, err := st.Get(id, "num")
+	assert.NoError(t, err)
+	assert.Equal(t, v, float64(123))
+
+	v, err = st.Get(id, "str")
+	assert.NoError(t, err)
+	assert.Equal(t, v, "hello 123")
+
+	v, err = st.Get(id, "bool")
+	assert.NoError(t, err)
+	assert.Equal(t, v, true)
+
+	// Non-existent field.
+	_, err = st.Get(id, "xx")
+	assert.ErrorIs(t, err, ErrFieldNotFound)
+
+	// Get multiple.
+	mp, err := st.GetMulti(id, "num", "str", "bool")
+	assert.NoError(t, err)
+	assert.Equal(t, mp, map[string]interface{}{
+		"str":  "hello 123",
+		"num":  float64(123),
+		"bool": true,
+	})
+	mp, err = st.GetMulti(id, "num", "str", "bool", "blah")
+	assert.ErrorIs(t, err, ErrFieldNotFound)
+
+	// Add another key in a different commit.
+	assert.NoError(t, st.Set(id, "num2", 456))
+	assert.NoError(t, st.Commit(id))
+
+	v, err = st.Get(id, "num2")
+	assert.NoError(t, err)
+	assert.Equal(t, v, float64(456))
+
+	// Delete.
+	assert.ErrorIs(t, st.Delete("blah", "num2"), ErrInvalidSession)
+	assert.NoError(t, st.Delete(id, "num2"))
+	v, err = st.Get(id, "num2")
+	v, err = st.Get(id, "num3")
+	assert.Error(t, ErrFieldNotFound)
+
+	// Clear.
+	assert.ErrorIs(t, st.Clear(randID), ErrInvalidSession)
+	assert.NoError(t, st.Clear(id))
+	v, err = st.Get(id, "str")
+	assert.Error(t, err, ErrFieldNotFound)
+}