{"id":46631622,"url":"https://github.com/soklet/toystore-app","last_synced_at":"2026-03-08T00:09:41.670Z","repository":{"id":179586785,"uuid":"662577694","full_name":"soklet/toystore-app","owner":"soklet","description":"An example Soklet application which demonstrates how to build a robust production system.","archived":false,"fork":false,"pushed_at":"2026-01-16T21:28:59.000Z","size":9862,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-17T10:19:11.452Z","etag":null,"topics":["example","soklet"],"latest_commit_sha":null,"homepage":"https://www.soklet.com/docs/toy-store-app","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/soklet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-07-05T12:38:04.000Z","updated_at":"2026-01-16T21:29:03.000Z","dependencies_parsed_at":null,"dependency_job_id":"8d46b229-2474-4f53-94e6-c5c0694c07bf","html_url":"https://github.com/soklet/toystore-app","commit_stats":null,"previous_names":["soklet/soklet-example-full","soklet/toystore-app"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/soklet/toystore-app","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soklet%2Ftoystore-app","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soklet%2Ftoystore-app/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soklet%2Ftoystore-app/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soklet%2Ftoystore-app/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/soklet","download_url":"https://codeload.github.com/soklet/toystore-app/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soklet%2Ftoystore-app/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30238250,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-07T23:52:25.683Z","status":"ssl_error","status_checked_at":"2026-03-07T23:52:25.373Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["example","soklet"],"created_at":"2026-03-08T00:09:39.326Z","updated_at":"2026-03-08T00:09:41.657Z","avatar_url":"https://github.com/soklet.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ca href=\"https://www.soklet.com/docs/toystore-app\"\u003e\n    \u003cpicture\u003e\n        \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://cdn.soklet.com/soklet-gh-logo-dark-v2.png\"\u003e\n        \u003cimg alt=\"Soklet\" src=\"https://cdn.soklet.com/soklet-gh-logo-light-v2.png\" width=\"300\" height=\"101\"\u003e\n    \u003c/picture\u003e\n\u003c/a\u003e\n\n### Soklet Toy Store App\n\nThis app showcases how you might build a real production backend using [Soklet](https://www.soklet.com) (a virtual-threaded Java HTTP + SSE server with zero dependencies).\n\nFeature highlights include:\n\n* Authentication (PBKDF2 using HMAC-SHA512 for password hashing, Ed25519 keypair for digital signatures)\n* Role-based authorization\n* Dependency injection via [Google Guice](https://github.com/google/guice)\n* Relational database integration via [Pyranid](https://www.pyranid.com)\n* Context-awareness via [ScopedValue (JEP 506)](https://openjdk.org/jeps/506)\n* Internationalization via [Lokalized](https://www.lokalized.com) and the JDK\n* JSON requests/responses via [Gson](https://github.com/google/gson)\n* Logging via [SLF4J](https://slf4j.org/) / [Logback](https://logback.qos.ch/)\n* Automated unit and integration tests via [JUnit](https://junit.org)\n* Ability to run in [Docker](https://www.docker.com/)\n\nThe app also includes a web frontend which makes it easy to kick the tires:\n\n![Toy Store App demo](https://cdn.soklet.com/soklet-toy-store-demo.gif)\n\nIf you'd like fewer moving parts, [a single-file \"barebones\" example is also available](https://github.com/soklet/barebones-app).\n\n**Note: this README provides a high-level overview of the Toy Store App.**\u003cbr/\u003e\n**For details, please refer to the official documentation at [https://www.soklet.com/docs/toystore-app](https://www.soklet.com/docs/toystore-app).**\n\n### Build and Run\n\nFirst, clone the Git repository and set your working directory.\n\n```shell\n% git clone git@github.com:soklet/toystore-app.git\n% cd toystore-app\n```\n\n#### With Docker\n\nThis is the easiest way to run the Toy Store App.  You don't need anything on your machine other than [Docker](https://www.docker.com).  The app will run in its own sandboxed Java 25 Docker Container.\n\n[The Dockerfile is viewable here](https://github.com/soklet/toystore-app/blob/main/docker/Dockerfile) if you are curious about how it works.\n\nYou likely will want to have your app run inside of a Docker Container using this approach in your real deployment environment.\n\n##### **Build**\n\n```shell\n% docker build . --file docker/Dockerfile --tag soklet/toystore\n```\n\n##### **Run**\n\n```shell\n# Press Ctrl+C to stop the interactive container session\n% docker run -e TOYSTORE_ENVIRONMENT=\"local\" -p 8080:8080 -p 8081:8081 soklet/toystore    \n```\n\n##### **Test**\n\n```shell\n% curl -i 'http://localhost:8080/'\nHTTP/1.1 200 OK\nContent-Length: 13\nContent-Type: text/plain; charset=UTF-8\nDate: Sun, 21 Mar 2024 16:19:01 GMT\n\nHello, world!\n```\n\n#### Without Docker\n\nThe Toy Store App requires [Apache Maven](https://maven.apache.org/) (you can skip Maven if you prefer to run directly through your IDE) and JDK 25+. If you need a JDK, [Amazon Corretto](https://aws.amazon.com/corretto/) is a free-to-use-commercially, production-ready distribution of [OpenJDK](https://openjdk.org/) that includes long-term support.\n\n##### **Build**\n\n```shell\n% mvn compile\n```\n\n##### **Run**\n\n```shell\n% 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\"\n```\n\n### API Demonstration\n\nHere we demonstrate how a client might interact with the Toy Store App.\n\n#### Authenticate\n\nGiven an email address and password, return account information and an authentication token (here, a [JWT](#jwt-handling)).\n\nWe specify `Accept-Language` and `Time-Zone` headers so the server knows how to provide \"friendly\" localized descriptions in the unauthenticated response.\n\n```shell\n % curl -i -X POST 'http://localhost:8080/accounts/authenticate' \\\n   -d '{\"emailAddress\": \"admin@soklet.com\", \"password\": \"administrator-password\"}' \\\n   -H \"Accept-Language: en-US\" \\\n   -H \"Time-Zone: America/New_York\"\nHTTP/1.1 200 OK\nContent-Length: 640\nContent-Type: application/json;charset=UTF-8\nDate: Sun, 09 Jun 2024 13:25:27 GMT\n\n{\n  \"authenticationToken\": \"eyJhbG...c76fxc\",\n  \"account\": {\n    \"accountId\": \"08d0ba3e-b19c-4317-a146-583860fcb5fd\",\n    \"roleId\": \"ADMINISTRATOR\",\n    \"name\": \"Example Administrator\",\n    \"emailAddress\": \"admin@soklet.com\",\n    \"timeZone\": \"America/New_York\",\n    \"timeZoneDescription\": \"Eastern Time\",\n    \"locale\": \"en-US\",\n    \"localeDescription\": \"English (United States)\",\n    \"createdAt\": \"2024-06-09T13:25:27.038870Z\",\n    \"createdAtDescription\": \"Jun 9, 2024, 9:25 AM\"\n  }\n}\n```\n\n#### Create Toy\n\nNow that we have an authentication token, add a toy to our database.\n\nBecause 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`).\n\n```shell\n# Note: price is a string instead of a JSON number (float)\n# to support exact arbitrary-precision decimals\n% curl -i -X POST 'http://localhost:8080/toys' \\\n  -d '{\"name\": \"Test\", \"price\": \"1234.5\", \"currency\": \"GBP\"}' \\\n  -H \"Authorization: Bearer eyJhbG...c76fxc\"\nHTTP/1.1 200 OK\nContent-Length: 351\nContent-Type: application/json;charset=UTF-8\nDate: Sun, 09 Jun 2024 13:44:26 GMT\n\n{\n  \"toy\": {\n    \"toyId\": \"9bd5ea4d-ebd1-47f7-a8b4-0531b8655e5d\",\n    \"name\": \"Test\",\n    \"price\": 1234.50,\n    \"priceDescription\": \"£1,234.50\",\n    \"currencyCode\": \"GBP\",\n    \"currencySymbol\": \"£\",\n    \"currencyDescription\": \"British Pound\",\n    \"createdAt\": \"2024-06-09T13:44:26.388364Z\",\n    \"createdAtDescription\": \"Jun 9, 2024, 9:44 AM\"\n  }\n}\n```\n\n#### Purchase Toy\n\nLet's purchase the toy that was just added.\n\n```shell\n % curl -i -X POST 'http://localhost:8080/toys/9bd5ea4d-ebd1-47f7-a8b4-0531b8655e5d/purchase' \\\n  -d '{\"creditCardNumber\": \"4111111111111111\", \"creditCardExpiration\": \"2028-03\"}' \\\n  -H \"Authorization: Bearer eyJhbG...c76fxc\"\nHTTP/1.1 200 OK\nContent-Length: 523\nContent-Type: application/json;charset=UTF-8\nDate: Sun, 09 Jun 2024 14:12:08 GMT\n\n{\n  \"purchase\": {\n    \"purchaseId\": \"9bd5ea4d-ebd1-47f7-a8b4-0531b8655e5d\",\n    \"accountId\": \"08d0ba3e-b19c-4317-a146-583860fcb5fd\",\n    \"toyId\": \"9bd5ea4d-ebd1-47f7-a8b4-0531b8655e5d\",\n    \"price\": 1234.50,\n    \"priceDescription\": \"£1,234.50\",\n    \"currencyCode\": \"GBP\",\n    \"currencySymbol\": \"£\",\n    \"currencyDescription\": \"British Pound\",\n    \"creditCardTransactionId\": \"72534075-d572-49fd-ae48-6c9644136e70\",\n    \"createdAt\": \"2024-06-09T14:12:08.100101Z\",\n    \"createdAtDescription\": \"Jun 9, 2024, 10:12 AM\"\n  }\n}\n```\n\n#### Internationalization (i18n)\n\nUnauthenticated 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).\n\n```shell\n% curl -i -X POST 'http://localhost:8080/toys' \\\n  -d '{\"name\": \"Bola de futebol\", \"price\": \"50\", \"currency\": \"BRL\"}' \\\n  -H \"Authorization: Bearer eyJhbG...c76fxc\"\nHTTP/1.1 200 OK\nContent-Length: 362\nContent-Type: application/json;charset=UTF-8\nDate: Sun, 09 Jun 2024 14:03:49 GMT\n\n{\n  \"toy\": {\n    \"toyId\": \"3c7c179a-a824-4026-b00c-811710192ff2\",\n    \"name\": \"Bola de futebol\",\n    \"price\": 50.00,\n    \"priceDescription\": \"R$ 50,00\",\n    \"currencyCode\": \"BRL\",\n    \"currencySymbol\": \"R$\",\n    \"currencyDescription\": \"Real brasileiro\",\n    \"createdAt\": \"2024-06-09T14:03:49.748571Z\",\n    \"createdAtDescription\": \"9 de jun. de 2024 11:03\"\n  }\n}\n```\n\nError messages are localized as well.  Here we supply a negative `price` and forget to specify a `currency`.\n\n```shell\n% curl -i -X POST 'http://localhost:8080/toys' \\\n  -d '{\"name\": \"Bola de futebol\", \"price\": \"-50\"}' \\ \n  -H \"Authorization: Bearer eyJhbG...c76fxc\"\nHTTP/1.1 422 Unprocessable Content\nContent-Length: 261\nContent-Type: application/json;charset=UTF-8\nDate: Sun, 09 Jun 2024 14:45:17 GMT\n\n{\n  \"summary\": \"O preço não pode ser negativo. A moeda é obrigatória.\",\n  \"generalErrors\": [],\n  \"fieldErrors\": {\n    \"price\": [\n      \"O preço não pode ser negativo.\"\n    ],\n    \"currency\": [\n      \"A moeda é obrigatória.\"\n    ]\n  },\n  \"metadata\": {}\n}\n```\n\n#### Server-Sent Events\n\nClients can listen on `/toys/event-source` for toy-related Server-Sent Events.\n\nNote that the standard Toy Store plumbing - authentication/authorization, transactions, etc. - automatically applies as you would expect for SSE Event Sources.\n\nServer-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).\n\nFirst, we ask for a short-lived, cryptographically-signed SSE access token for the authenticated account:\n\n```shell\n% curl -i -X POST 'http://localhost:8080/accounts/sse-access-token' \\\n  -H \"Authorization: Bearer eyJhbG...c76fxc\"\nHTTP/1.1 200 OK\nContent-Length: 351\nContent-Type: application/json;charset=UTF-8\nDate: Sun, 09 Jun 2024 13:44:26 GMT\n\n{\n  \"accessToken\": \"eyJ...KDA\"\n}\n```\n\nIn-browser, you'd use a standard JS [`Event Source`](https://html.spec.whatwg.org/multipage/server-sent-events.html#eventsource) to subscribe to Server-Sent Events.\n\nHere, we use `netcat` to listen from the console:\n\n```shell\n% echo -ne 'GET /toys/event-source?sse-access-token=eyJ...KDA HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n' | netcat localhost 8081\n\nHTTP/1.1 200 OK\nContent-Type: text/event-stream; charset=UTF-8\nCache-Control: no-cache\nCache-Control: no-transform\nConnection: keep-alive\nX-Accel-Buffering: no\nDate: Fri, 12 Dec 2025 22:19:01 GMT\n\n:\n\nevent: toy-purchased\ndata: {\ndata:   \"toy\": {\ndata:     \"toyId\": \"036bd776-3ad2-4b0b-9f58-63aff05946aa\",\ndata:     \"name\": \"teddy\",\ndata:     \"price\": 10.25,\ndata:     \"priceDescription\": \"£ 10,25\",\ndata:     \"currencyCode\": \"GBP\",\ndata:     \"currencySymbol\": \"£\",\ndata:     \"currencyDescription\": \"Libra esterlina\",\ndata:     \"createdAt\": \"2025-12-12T22:19:00.421502Z\",\ndata:     \"createdAtDescription\": \"12 de dez. de 2025 19:19\"\ndata:   },\ndata:   \"purchase\": {\ndata:     \"purchaseId\": \"036bd776-3ad2-4b0b-9f58-63aff05946aa\",\ndata:     \"accountId\": \"08d0ba3e-b19c-4317-a146-583860fcb5fd\",\ndata:     \"toyId\": \"036bd776-3ad2-4b0b-9f58-63aff05946aa\",\ndata:     \"price\": 10.25,\ndata:     \"priceDescription\": \"£ 10,25\",\ndata:     \"currencyCode\": \"GBP\",\ndata:     \"currencySymbol\": \"£\",\ndata:     \"currencyDescription\": \"Libra esterlina\",\ndata:     \"creditCardTransactionId\": \"b86de979-d080-496c-9df8-116e401b4379\",\ndata:     \"createdAt\": \"2025-12-12T22:19:35.679794Z\",\ndata:     \"createdAtDescription\": \"12 de dez. de 2025 19:19\"\ndata:   }\ndata: }\n```\n\n### Learning More\n\nPlease refer to the official Soklet website [https://www.soklet.com](https://www.soklet.com) for detailed documentation.\n\nThe Toy Store App has its own dedicated section at [https://www.soklet.com/docs/toystore-app](https://www.soklet.com/docs/toystore-app).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoklet%2Ftoystore-app","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoklet%2Ftoystore-app","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoklet%2Ftoystore-app/lists"}