This app showcases how you might build a real production backend using Soklet (a virtual-threaded Java HTTP + SSE server with zero dependencies).
Feature highlights include:
- Authentication (PBKDF2 using HMAC-SHA512 for password hashing, Ed25519 keypair for digital signatures)
- Role-based authorization
- Dependency injection via Google Guice
- Relational database integration via Pyranid
- Context-awareness via ScopedValue (JEP 506)
- Internationalization via Lokalized and the JDK
- JSON requests/responses via Gson
- Server-Sent Events (SSE) and Model Context Protocol (MCP)
- Logging via SLF4J / Logback
- Automated unit and integration tests via JUnit
- Ability to run in Docker
The app also includes a web frontend which makes it easy to kick the tires:
If you'd like fewer moving parts, a single-file "barebones" example is also available.
Note: this README provides a high-level overview of the Toy Store App.
For details, please refer to the official documentation at https://www.soklet.com/docs/toystore-app.
First, clone the Git repository and set your working directory.
% git clone [email protected]:soklet/toystore-app.git
% cd toystore-appThis is the easiest way to run the Toy Store App. You don't need anything on your machine other than Docker. The app will run in its own sandboxed Java 25 Docker Container.
The Dockerfile is viewable here if you are curious about how it works.
You likely will want to have your app run inside of a Docker Container using this approach in your real deployment environment.
% docker build . --file docker/Dockerfile --tag soklet/toystore# Press Ctrl+C to stop the interactive container session
% docker run -e TOYSTORE_ENVIRONMENT="local" -p 8080:8080 -p 8081:8081 -p 8082:8082 soklet/toystoreThis starts the regular HTTP API on port 8080, the SSE server on 8081, and the MCP server on 8082.
% curl -i 'http://localhost:8080/'
HTTP/1.1 200 OK
Content-Length: 13
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Hello, world!The Toy Store App requires Apache Maven (you can skip Maven if you prefer to run directly through your IDE) and JDK 25+. If you need a JDK, Amazon Corretto is a free-to-use-commercially, production-ready distribution of OpenJDK that includes long-term support.
% mvn compile% TOYSTORE_ENVIRONMENT="local" MAVEN_OPTS="--sun-misc-unsafe-memory-access=allow --enable-native-access=ALL-UNNAMED --add-opens java.base/java.time=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED" mvn -e exec:java -Dexec.mainClass="com.soklet.toystore.App"Here we demonstrate how a client might interact with the Toy Store App.
Given an email address and password, return account information and an authentication token (here, a JWT).
We specify Accept-Language and Time-Zone headers so the server knows how to provide "friendly" localized descriptions in the unauthenticated response.
% curl -i -X POST 'http://localhost:8080/accounts/authenticate' \
-d '{"emailAddress": "[email protected]", "password": "administrator-password"}' \
-H "Accept-Language: en-US" \
-H "Time-Zone: America/New_York"
HTTP/1.1 200 OK
Content-Length: 640
Content-Type: application/json;charset=UTF-8
Date: Sun, 09 Jun 2024 13:25:27 GMT
{
"authenticationToken": "eyJhbG...c76fxc",
"account": {
"accountId": "08d0ba3e-b19c-4317-a146-583860fcb5fd",
"roleId": "ADMINISTRATOR",
"name": "Example Administrator",
"emailAddress": "[email protected]",
"timeZone": "America/New_York",
"timeZoneDescription": "Eastern Time",
"locale": "en-US",
"localeDescription": "English (United States)",
"createdAt": "2024-06-09T13:25:27.038870Z",
"createdAtDescription": "Jun 9, 2024, 9:25 AM"
}
}Now that we have an authentication token, add a toy to our database.
Because the server knows which account is making the request, the data in the response is formatted according to the account's preferred locale and timezone (here, en-US and America/New_York).
# Note: price is a string instead of a JSON number (float)
# to support exact arbitrary-precision decimals
% curl -i -X POST 'http://localhost:8080/toys' \
-d '{"name": "Test", "price": "1234.5", "currency": "GBP"}' \
-H "Authorization: Bearer eyJhbG...c76fxc"
HTTP/1.1 200 OK
Content-Length: 351
Content-Type: application/json;charset=UTF-8
Date: Sun, 09 Jun 2024 13:44:26 GMT
{
"toy": {
"toyId": "9bd5ea4d-ebd1-47f7-a8b4-0531b8655e5d",
"name": "Test",
"price": 1234.50,
"priceDescription": "£1,234.50",
"currencyCode": "GBP",
"currencySymbol": "£",
"currencyDescription": "British Pound",
"createdAt": "2024-06-09T13:44:26.388364Z",
"createdAtDescription": "Jun 9, 2024, 9:44 AM"
}
}Let's purchase the toy that was just added.
% curl -i -X POST 'http://localhost:8080/toys/9bd5ea4d-ebd1-47f7-a8b4-0531b8655e5d/purchase' \
-d '{"creditCardNumber": "4111111111111111", "creditCardExpiration": "2028-03"}' \
-H "Authorization: Bearer eyJhbG...c76fxc"
HTTP/1.1 200 OK
Content-Length: 523
Content-Type: application/json;charset=UTF-8
Date: Sun, 09 Jun 2024 14:12:08 GMT
{
"purchase": {
"purchaseId": "9bd5ea4d-ebd1-47f7-a8b4-0531b8655e5d",
"accountId": "08d0ba3e-b19c-4317-a146-583860fcb5fd",
"toyId": "9bd5ea4d-ebd1-47f7-a8b4-0531b8655e5d",
"price": 1234.50,
"priceDescription": "£1,234.50",
"currencyCode": "GBP",
"currencySymbol": "£",
"currencyDescription": "British Pound",
"creditCardTransactionId": "72534075-d572-49fd-ae48-6c9644136e70",
"createdAt": "2024-06-09T14:12:08.100101Z",
"createdAtDescription": "Jun 9, 2024, 10:12 AM"
}
}Unauthenticated requests use Accept-Language and Time-Zone headers; authenticated requests use the account's locale and time zone. The example below assumes the account is configured for pt-BR (Brazilian Portuguese) and America/Sao_Paulo (São Paulo time, UTC-03:00).
% curl -i -X POST 'http://localhost:8080/toys' \
-d '{"name": "Bola de futebol", "price": "50", "currency": "BRL"}' \
-H "Authorization: Bearer eyJhbG...c76fxc"
HTTP/1.1 200 OK
Content-Length: 362
Content-Type: application/json;charset=UTF-8
Date: Sun, 09 Jun 2024 14:03:49 GMT
{
"toy": {
"toyId": "3c7c179a-a824-4026-b00c-811710192ff2",
"name": "Bola de futebol",
"price": 50.00,
"priceDescription": "R$ 50,00",
"currencyCode": "BRL",
"currencySymbol": "R$",
"currencyDescription": "Real brasileiro",
"createdAt": "2024-06-09T14:03:49.748571Z",
"createdAtDescription": "9 de jun. de 2024 11:03"
}
}Error messages are localized as well. Here we supply a negative price and forget to specify a currency.
% curl -i -X POST 'http://localhost:8080/toys' \
-d '{"name": "Bola de futebol", "price": "-50"}' \
-H "Authorization: Bearer eyJhbG...c76fxc"
HTTP/1.1 422 Unprocessable Content
Content-Length: 261
Content-Type: application/json;charset=UTF-8
Date: Sun, 09 Jun 2024 14:45:17 GMT
{
"summary": "O preço não pode ser negativo. A moeda é obrigatória.",
"generalErrors": [],
"fieldErrors": {
"price": [
"O preço não pode ser negativo."
],
"currency": [
"A moeda é obrigatória."
]
},
"metadata": {}
}Clients can listen on /toys/event-source for toy-related Server-Sent Events.
Note that the standard Toy Store plumbing - authentication/authorization, transactions, etc. - automatically applies as you would expect for SSE Event Sources.
Server-Sent Events, per spec, do not support custom headers, so we mint a short-lived SSE access token that is safe to pass to our Event Source Method as a query parameter (this mitigates replay attacks that would be possible if we were to pass the long-lived Access Token instead).
First, we ask for a short-lived, cryptographically-signed SSE access token for the authenticated account:
% curl -i -X POST 'http://localhost:8080/accounts/sse-access-token' \
-H "Authorization: Bearer eyJhbG...c76fxc"
HTTP/1.1 200 OK
Content-Length: 351
Content-Type: application/json;charset=UTF-8
Date: Sun, 09 Jun 2024 13:44:26 GMT
{
"accessToken": "eyJ...KDA"
}In-browser, you'd use a standard JS Event Source to subscribe to Server-Sent Events.
Here, we use netcat to listen from the console:
% echo -ne 'GET /toys/event-source?sse-access-token=eyJ...KDA HTTP/1.1\r\nHost: localhost\r\n\r\n' | netcat localhost 8081
HTTP/1.1 200 OK
Content-Type: text/event-stream; charset=UTF-8
Cache-Control: no-cache
Cache-Control: no-transform
Connection: keep-alive
X-Accel-Buffering: no
Date: Fri, 12 Dec 2025 22:19:01 GMT
:
event: toy-purchased
data: {
data: "toy": {
data: "toyId": "036bd776-3ad2-4b0b-9f58-63aff05946aa",
data: "name": "teddy",
data: "price": 10.25,
data: "priceDescription": "£ 10,25",
data: "currencyCode": "GBP",
data: "currencySymbol": "£",
data: "currencyDescription": "Libra esterlina",
data: "createdAt": "2025-12-12T22:19:00.421502Z",
data: "createdAtDescription": "12 de dez. de 2025 19:19"
data: },
data: "purchase": {
data: "purchaseId": "036bd776-3ad2-4b0b-9f58-63aff05946aa",
data: "accountId": "08d0ba3e-b19c-4317-a146-583860fcb5fd",
data: "toyId": "036bd776-3ad2-4b0b-9f58-63aff05946aa",
data: "price": 10.25,
data: "priceDescription": "£ 10,25",
data: "currencyCode": "GBP",
data: "currencySymbol": "£",
data: "currencyDescription": "Libra esterlina",
data: "creditCardTransactionId": "b86de979-d080-496c-9df8-116e401b4379",
data: "createdAt": "2025-12-12T22:19:35.679794Z",
data: "createdAtDescription": "12 de dez. de 2025 19:19"
data: }
data: }The Toy Store App also exposes a read-only MCP endpoint on port 8082 at /mcp.
Like SSE, MCP uses its own short-lived audience-specific token instead of the long-lived API token. First, ask the HTTP API to mint one:
% curl -i -X POST 'http://localhost:8080/accounts/mcp-access-token' \
-H "Authorization: Bearer eyJhbG...c76fxc"
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
"accessToken": "eyJ...mcp"
}Next, initialize an MCP session with the MCP token:
% curl -i -X POST 'http://localhost:8082/mcp' \
-H "Authorization: Bearer eyJ...mcp" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0",
"id":"req-1",
"method":"initialize",
"params":{
"protocolVersion":"2025-11-25",
"capabilities":{},
"clientInfo":{"name":"curl","version":"1.0.0"}
}
}'Copy the MCP-Session-Id response header, then finish initialization and call a tool:
% curl -X POST 'http://localhost:8082/mcp' \
-H "Content-Type: application/json" \
-H "MCP-Session-Id: s_abc123" \
-H "MCP-Protocol-Version: 2025-11-25" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}'
% curl -X POST 'http://localhost:8082/mcp' \
-H "Content-Type: application/json" \
-H "MCP-Session-Id: s_abc123" \
-H "MCP-Protocol-Version: 2025-11-25" \
-d '{
"jsonrpc":"2.0",
"id":"req-2",
"method":"tools/call",
"params":{
"name":"list_toys",
"arguments":{}
}
}'The first MCP surface is intentionally narrow:
list_toysget_toyresources/listresources/readfortoystore://toys/{toyId}
Please refer to the official Soklet website https://www.soklet.com for detailed documentation.
The Toy Store App has its own dedicated section at https://www.soklet.com/docs/toystore-app.
