ShopiTerm
ShopiTerm is an ecommerce platform that is unlike those you are used to.
It is designed to be used from your terminal, with efficiency in mind.
With a few keyboard inputs, you have bought your favorite item.
The application is accessed by SSH with no need to install or run any other software than an SSH client.
This documentation gives an overview over the whole software system, how to install, configure and run it.
Additionally, it provides details on how to interact with ShopiTerm and what our development workflow looks like.
Use the navigation on the side to view the respective pages.
Architecture
The ShopiTerm software system consists of multiple microservices.
This page lists them and gives some detail on their implementation.
For running and configuring them, see the installation and configuration pages.
External Dependencies
We use two main external dependencies in this software system: NATS and PostgreSQL.
NATS
NATS is a messaging system used by ShopiTerm for event-driven interactions.
It offers a simple-to-use interface to subscribe to and publish messages on so-called subjects.
This allows us to notify other services when something needs to be started or has finished.
PostgreSQL
PostgreSQL is the database management system used by ShopiTerm.
Each service that needs persistence in the form of a database uses it.
We use the diesel library for interfacing with PostgreSQL.
This gives us strong compile-time guarantees about the correctness of our queries.
All services use either an asynchronous variant of diesel or a connection pool, ensuring fast handling of concurrent requests to our system.
Where necessary (e.g. when updating stock information) we use transactions to ensure that operations consisting of multiple database operations result in a consistent state.
Catalogue
The catalogue service keeps track of the items available within the store. It does not track actual stock information but rather the metadata about a given item. An item has the attributes one would expect from a shop such as name, description and price. Additionally, the catalogue offers the ability to add arbitrary tags to an item.
Warehouse
The warehouse component manages the actual stock of items for ShopiTerm.
There can be any number of warehouses, each with their own stock of items.
Warehouses can be created and deleted and items can be added or removed from them.
In order to support the ordering process, items can be reserved at a warehouse before an order is finalized.
This prevents a single item instance to be sold multiple times.
Orders
The order service handles checkout and keeps the canonical record of placed orders.
It accepts a CreateOrder payload, validates the shipping address and ordered items, prices the items, reserves the required stock in the warehouse service and stores the order together with its items and shipping address in PostgreSQL.
Order creation is transactional: if persisting the order fails, any reservations are rolled back. Once the order has been written successfully, the service publishes the placed order on NATS so that the payment service can create a payment flow for it. The service also exposes endpoints to fetch an order, its items and payment information, and to mark an order as shipped. It also publishes an event to NATS when the order is marked as shipped for the notification service to notify the client that the command is shipped.
Notificator
The notification service sends email confirmations to customers at key stages of the order lifecycle. It listens to NATS events published by the payment and orders services, fetches the corresponding order details from the order service, and sends customer emails via SMTP. The service sends three types of notifications: order confirmation emails when a payment is created, payment confirmation emails when payment is marked as paid, and shipping confirmation emails when an order is marked as shipped.
Payment
The payment service listens for newly created orders via NATS and creates a Taler payment for them.
Taler returns a payment URI that is announced on NATS and can then be displayed to customers for payment with their Taler wallet.
All currently open payments are tracked using a NATS key-value store.
Once a payment is marked as paid by Taler, the corresponding ShopiTerm order is announced to be paid via NATS.
Recommendations
The recommendation service generates product suggestions for ShopiTerm customers.
It exposes three strategies over HTTP: globally popular items, items similar to a given product, and personalized picks for the current user.
A local mirror of the catalogue’s product metadata is kept up to date via NATS events from the catalogue, and the popular and similar rankings are pre-computed on a schedule so read paths are cheap.
Similarity is exact-match Jaccard on (key, value) tag pairs — there is no NLP and no embeddings, so the quality of the similar lists depends directly on the catalogue’s tag taxonomy.
Personalized recommendations mix two signals over a recent-interactions window: items similar to those the user has recently engaged with, and globally popular items the user has not yet seen.
Interactions (view, add_to_cart, purchase) are weighted differently when scoring popularity, with purchases counting the heaviest.
The TUI client consumes these endpoints in two places: a Similar items panel inside the item detail view, and a You might also like block on the order confirmation screen.
TUI Client
The user interface to ShopiTerm is built using ratatui.
It is exported as a library to be used by the SSH server.
The interface consists of multiple views, one for each task such as catalogue browsing or doing the payment.
A loop-based renderer then uses these views to assemble the user interface.
Keyboard input is handled based on the currently active view.
The TUI is implemented as a state machine with a View enum that tracks which screen the user is on.
Views include the item list, search, cart, checkout, payment (with QR code display), order confirmation, and a full admin panel for managing items, warehouses, and orders.
Keyboard input is dispatched based on the active view, and the renderer redraws the current view on each event.
The client library also integrates with the recommendations service, displaying similar items in the item detail view and personalized suggestions on the order confirmation screen.
Only items with available stock are shown to customers, and cart quantities are capped at the available stock level.
SSH Server
The SSH server is the main entry point for users.
It accepts SSH connections using public-key authentication and hosts the TUI client for each connected session.
When a user connects, their SSH key fingerprint is registered in a PostgreSQL database.
Admin users are identified by matching against a configured admin public key.
Upon session creation, the server generates a JWT token (HS256) containing the user ID and role, which is used for authenticated API requests through the gateway.
The server manages terminal state via russh, translating raw SSH input bytes into keyboard events and piping ratatui output back over the SSH channel.
It also spawns a background payment poller to provide live payment status updates within the TUI.
Sessions have a one-hour inactivity timeout.
Common Library
The shopiterm_common is a Rust library helping us in building the whole project.
It is mainly a collection of data structures that are exchanged between services.
These structures are set up for serialization and deserialization, ensuring easy re-use between services.
API Gateway
The API gateway uses KrakenD to route HTTP traffic from clients to the backend microservices.
It listens on port 8080 and exposes a unified API surface covering the catalogue, orders, warehouse, and recommendations services.
Mutating endpoints (create, update, delete) are protected by JWT validation (HS256, issuer shopiterm).
The gateway also injects the X-User-Id header from the JWT sub claim for personalized recommendation endpoints.
Read-only endpoints are publicly accessible without authentication.
API Client
The API client is not an actual service but rather a library used by the services to exchange information. It provides the ability to make authenticated requests to the gateway and fetch all the different data structures used by the different services.
Installation
Tip
This explains all options in detail. To just get started with a working setup, use the compose file.
We offer three way to run ShopiTerm.
The easiest to get started is using our provided containers via compose.
When using NixOS, there is the option to use preconfigured NixOS modules.
Note
For the instructions in this documentation we are using Podman as the container tool, but you can use the same commands for Docker.
Containers
Each of our services is containerized and available at Quay.io. The following images are available:
| Service | Image |
|---|---|
| catalogue | quay.io/asefs26-shopiterm/catalogue |
| notificator | quay.io/asefs26-shopiterm/notificator |
| orders | quay.io/asefs26-shopiterm/orders |
| payment | quay.io/asefs26-shopiterm/payment |
| recommendations | quay.io/asefs26-shopiterm/recommendations |
| ssh | quay.io/asefs26-shopiterm/ssh |
| warehouse | quay.io/asefs26-shopiterm/warehouse |
| krakend (Gateway) | quay.io/asefs26-shopiterm/krakend |
Tags are available per branch and cpu architecture.
Currently, we only provide images for the amd64 architecture.
As an example, to run the latest catalogue image built of the main branch for amd64 you can use:
podman run quay.io/asefs26-shopiterm/catalogue:main-amd64 --help
This will print all available options of the catalogue service.
Caution
To function properly, most services require external dependencies and also depend on each other. We therefore provide a compose file for a simple setup.
Compose
We provide a docker-compose.yaml file for container composition using Podman / Docker Compose.
It starts each service and the required dependencies, including a PostgreSQL database for each service that uses it.
To start it, simply run the following in the root of the repository:
podman compose up
Tip
The compose setup creates empty databases without any seed data. To test the shop with some pre-filled data, use the interactive tester instead.
Note
The preconfigured port for SSH access is
2222.
NixOS Modules
We provide NixOS modules to configure ShopiTerm on a NixOS system.
Each service comes with its own module.
For usage with flakes, first import the project:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
shopiterm = {
url = "github:asefs26/team-6";
inputs.nixpkgs.follows = "nixpkgs";
};
};
}
Then import the modules to configure to your NixOS configuration.
The name of each service module is the same as the service itself.
For example, the module for the catalogue service is shopiterm.nixosModules.catalogue.
You can check the modules for configuration details. As an example, the catalogue service can be configured as follows:
{
services.shopiTerm.catalogue =
let
cataloguePort = 3001;
warehousePort = 3002;
in
{
enable = true;
host = "[::]";
port = cataloguePort;
# Create a local PostgreSQL DB automatically.
createDB = true;
warehouse = "http://localhost:${toString warehousePort}";
};
}
From Source
Lastly, there is the option to build the project directly from source. To do so, there are two options:
Nix
Tip
Installation instructions for Nix can be found on its website.
This project is developed using Nix. The project can be built by running the following:
nix build -L
The resulting binaries will be placed at result/bin.
Cargo
Tip
A Rust toolchain can be installed using rustup.
Alternatively, the Rust package manager Cargo can be used. Run the following for a build of all services:
cargo build --release
Configuration
All ShopiTerm services can be configured through environment variables.
This page lists all available configuration options per service.
Catalogue
| Variable | Description | Default |
|---|---|---|
LISTEN | Address on which to listen on | localhost:3000 |
DB | Address of PostgreSQL instance | postgres:///shopiterm_catalogue?host=/var/run/postgresql |
WAREHOUSE | Address of the warehouse service | http://localhost:3002 |
NATS | Address of the NATS server | localhost:4222 |
Warehouse
| Variable | Description | Default |
|---|---|---|
LISTEN | Address on which to listen on | localhost:3000 |
DB | Address of PostgreSQL instance | postgres:///shopiterm_warehouse?host=/var/run/postgresql |
CATALOGUE | Address of the catalogue service | http://localhost:3001 |
Orders
| Variable | Description | Default |
|---|---|---|
LISTEN | Address on which to listen on | localhost:3000 |
DB | Address of PostgreSQL instance | postgres:///shopiterm_orders?host=/var/run/postgresql |
NATS | Address of NATS instance | localhost:4222 |
WAREHOUSE | Address of warehouse service | http://localhost:3002 |
CATALOGUE | Address of catalogue service | http://localhost:3001 |
Notificator
| Variable | Description | Default |
|---|---|---|
NATS | Address of the NATS server | localhost:4222 |
MAILER_HOST | Address of the mailer | localhost |
MAILER_PORT | Port of the mailer | 1025 |
ORDER | Address of order service | http://localhost:3003 |
Payment
| Variable | Description | Default |
|---|---|---|
TALER_BACKEND | Address of the Taler merchant backend | https://backend.demo.taler.net |
TALER_INSTANCE | Instance on the Taler merchant backend to use | sandbox |
TALER_TOKEN | Bearer token to use for Taler authorization | secret-token:sandbox |
NATS | Address of the NATS server | nats://localhost:4222 |
Recommendations
| Variable | Description | Default |
|---|---|---|
LISTEN | Address to bind the HTTP listener on | localhost:3003 |
DATABASE_URL | PostgreSQL connection URL | required |
CATALOGUE_URL | Base URL of the catalogue service | http://localhost:3001 |
NATS_URL | NATS server URL for the JetStream product-events stream | nats://localhost:4222 |
RECOMPUTE_INTERVAL_SECS | Interval in seconds between background recomputation passes (0 disables) | 300 |
WEIGHT_VIEW | Score weight for a “view” interaction | 1.0 |
WEIGHT_ADD_TO_CART | Score weight for an “add_to_cart” interaction | 4.0 |
WEIGHT_PURCHASE | Score weight for a “purchase” interaction | 10.0 |
PERSONALIZED_WINDOW_DAYS | Window in days of recent interactions used as seed signal for personalized recommendations | 30 |
SSH Server
| Variable | Description | Default |
|---|---|---|
LISTEN | Address on which to listen for SSH connections | 0.0.0.0:2222 |
GATEWAY_URL | Base URL of the gateway API | http://localhost:8080 |
HOST_KEY_FILE | Path to an OpenSSH private key file for the host key | optional |
HOST_KEY | PEM string for the host key (alternative to HOST_KEY_FILE) | optional |
DB | PostgreSQL connection URL | postgres:///shopiterm_ssh?host=/var/run/postgresql |
JWT_SECRET | JWT secret for authentication | YS1zdHJpbmctc2VjcmV0LWF0LWxlYXN0LTI1Ni1iaXRzLWxvbmc |
Usage
With ShopiTerm installed and configured, it can be used via SSH.
Assuming it is configured as with the default compose file, ShopiTerm listens on port 2222.
ssh localhost -p 2222
Catalogue
After connecting via SSH, you are presented with the item list view showing all available products.
Use ↑ and ↓ to navigate through items.
Press Enter to view the details of a selected item, including its description, price, tags, and a sidebar of similar item recommendations.
Press / to open the search view, where you can type to fuzzy-filter items by name.
Press Esc to return to the previous view.
Cart
Press a to add the currently focused item to your cart.
Navigate to the cart view to see all items you have added.
Use ↑ and ↓ to navigate cart items, and adjust quantities with + and -.
Quantities are capped at the available stock level, so you cannot add more than what is in stock.
Press d or Delete to remove an item from the cart.
Checkout
From the cart view, press Enter to proceed to checkout.
You will be prompted to fill in a shipping address form with fields for street, city, postal code, and country (selected from a list).
An email address is also required for order notifications.
Use Tab to move between fields and Enter to submit the form.
The system validates all fields before allowing you to proceed.
Payment
With the checkout completed, the payment process is being started. As we try to use as much free and open source technology as possible, we opted for GNU Taler as our payment solution. There is no need to enter any payment details. Instead, a QR code will be generated which can be scanned by a Taler wallet. Once paid through Taler, the order is automatically confirmed.
Note
By default,
ShopiTermuses the demo backed hosted by Taler with the KUDOS demo currency. This is already configured in the official Taler mobile wallets for usage.
Adding Items
Admin users (identified by a configured SSH public key) are presented with an admin menu upon connecting. From the admin menu, you can manage items, warehouses, and orders.
To add a new item, navigate to the item list in the admin panel and select the option to create a new item. Fill in the item details such as name, description, and price, then submit. Tags can be added or removed from items to improve catalogue organization and recommendation quality. Stock for each item is managed per warehouse through the warehouse management view.
Order Overview
Admin users can view all placed orders from the admin panel. The order list displays each order with its status (e.g. awaiting payment, paid, shipped). Select an order to view its details, including the ordered items, shipping address, and payment information. From the order detail view, admins can mark an order as shipped, which triggers a shipping confirmation email to the customer.
Recommendations
Recommendations are displayed in two places as you browse:
- Item Detail (Similar items): when an item is focused in the catalogue or search view, the right column shows a list of items the recommender considers similar based on shared tags.
This refreshes as you navigate with
↑/↓. - Order Confirmation (You might also like): after a successful purchase, the confirmation screen lists personalized picks mixing items similar to your recent activity with popular items you have not yet seen.
Use
↑/↓to navigate the list; pressingEnteron a highlighted recommendation resets the session and focuses that product in the catalogue for your next round.
Note
Recommendations are served from precomputed tables that refresh on a configurable interval. Brand-new items or interactions may take up to one refresh cycle to appear in the Similar and You might also like lists.
Development
This page describes our development workflow for building ShopiTerm.
Project Structure
ShopiTerm is a Rust project organized as a workspace with multiple components:
shopiterm_api_client— API client (library)shopiterm_catalogue— Inventory managementshopiterm_client— TUI client (library)shopiterm_common— Commonly shared data structures (library)shopiterm_notificator— Email notificationsshopiterm_orders— Order processingshopiterm_payment— Payment handling with Talershopiterm_recommendations— Product recommendation engineshopiterm_ssh— SSH terminal interfaceshopiterm_warehouse— Stock and warehouse management
For details on the architecture of ShopiTerm, see the corresponding page.
Prerequisites
Building
The entire project is built using a Nix flake. To build the whole project, use:
nix build
The resulting binaries are placed at result.
To build specific services, use:
nix build .#catalogue
nix build .#warehouse
nix build .#orders
Tip
When building via Nix, unit tests are automatically run after the build.
Formatting
We use treefmt with:
nixfmt— Nix code formattingrustfmt— Rust code formatting
Format all code:
nix fmt
Linting
Rust linting using clippy is performed when doing a Nix check:
nix flake check -L
Testing
Unit Tests
As stated above, unit tests are automatically run on a Nix build. When having a Rust toolchain installed, they can also be run via Cargo:
cargo test --workspace
Integration Tests
Integration tests are implemented as Nix testers.
These tests spin up virtual machines with the ShopiTerm services and the respective databases configured.
They live in nix/testers.
Inside nix/testers/lib you can find utilities for creating curl requests and mock instances of API items.
nix build .#catalogueTest
nix build .#recommendationsTest
nix build .#sshTest
nix build .#warehouseTest
Note
These integration tests are not run during a nix build but have to be specifically called.
Live System
There is a special tester running the whole software system for interactive testing:
nix run .#interactiveTest
Note
The preconfigured port for SSH access is
2222and the gateway is accessible at port8080.
HTTP API Documentation
Some services provide an OpenAPI specification and a browser for it. These include:
- catalogue
- warehouse
For these services, the OpenAPI specification is available at: /openapi.json and can be browsed using the bundled UI at /api-doc.
Note
These paths reference the service itself, not behind the gateway.
Git Commit Messages
We enforce Conventional Commits using commitlint:
nix run .#commitlint
Continuous Integration
GitHub Actions (.github/workflows/CI.yaml) runs on every push:
| Job | Description |
|---|---|
| Check | Validates commit messages, runs nix flake check (format + lint) |
| Build | Builds all packages with nix build -L |
| Test | Runs Nix integration tests |
| Container | Builds and pushes container images to registry |
Containers
Each service comes with a container that can be built using Nix:
nix build .#<service>Container
The result will be an OCI image that can be imported:
podman load < result
Note
Images for each branch are pushed to Quay.io by CI.
Caching
We use Cachix for caching Nix builds. It is enabled in CI, but can also be used locally:
nix run nixpkgs#cachix use shopiterm
Workflow Summary
Here is an overview of what the workflow looks like:
# changes ...
↓
# build
nix build -L
↓
# format
nix fmt
↓
# lint & check
nix flake check -L
↓
# (when related) integration tests
nix build .#<service>Test
↓
# push
git push
↓
# CI checks all above stages (+ commit message)
↓
# CI builds & pushes containers