HappyQOTD serves a daily quote through the classic Quote of the Day protocol over TCP and UDP, plus an ASP.NET Core HTTP API. The daily quote is selected by UTC date and persisted in SQLite, so all three services return the same quote.
Note
The included seed database contains only a small set of SFW sample quotes. It is separate from the quote collection used by the public instance.
- TCP Quote of the Day server on port
17 - UDP Quote of the Day server on port
17 - ASP.NET Core Minimal API
- SQLite quote storage
- Optional Mission Control telemetry
The local development URL is http://localhost:5269. The production deployment in the VPS compose stack listens on http://127.0.0.1:5193.
| Method | Path | Authentication | Description |
|---|---|---|---|
GET |
/ |
None | Returns HappyQOTD. |
GET |
/health/live |
None | Liveness check. |
GET |
/health/ready |
None | Readiness check. Verifies that SQLite can be opened and queried. |
GET |
/api/quotes/today |
None | Returns the quote selected for the current UTC date. Returns 404 when no quote is available. |
GET |
/api/quotes/random |
None | Returns a random active quote. Returns 404 when no quote is available. |
POST |
/api/quotes |
X-HappyQOTD-Key |
Creates one quote. |
POST |
/api/quotes/batch |
X-HappyQOTD-Key |
Creates multiple quotes. |
PUT |
/api/quotes/today |
X-HappyQOTD-Key |
Sets the current UTC day's quote. |
DELETE |
/api/quotes/{id} |
X-HappyQOTD-Key |
Deletes a quote by numeric ID. |
Write requests return 503 when no admin key is configured and 401 when the supplied key is missing or incorrect. Write endpoints are limited to 5 requests per minute. Read endpoints are limited to 120 requests per minute per client, except loopback clients.
POST /api/quotes accepts:
{
"text": "Readable code is a favor you leave for your future self.",
"author": "",
"source": "HappyQOTD original"
}Quote validation rules:
textis required and may contain up to 1,000 characters.authormay contain up to 200 characters.sourcemay contain up to 300 characters.
POST /api/quotes/batch accepts an array of the same request objects:
[
{
"text": "First quote",
"author": "Author",
"source": "Example"
},
{
"text": "Second quote"
}
]PUT /api/quotes/today accepts:
{
"quoteId": 4
}DELETE /api/quotes/{id} returns 204 when the quote is deleted and 404 when the ID does not exist.
Read today's quote:
curl http://localhost:5269/api/quotes/todayCreate a quote:
curl -X POST http://localhost:5269/api/quotes \
-H "Content-Type: application/json" \
-H "X-HappyQOTD-Key: ${HAPPYQOTD_ADMIN_API_KEY}" \
-d '{"text":"A useful quote","author":"An author","source":"Example"}'Set today's quote:
curl -X PUT http://localhost:5269/api/quotes/today \
-H "Content-Type: application/json" \
-H "X-HappyQOTD-Key: ${HAPPYQOTD_ADMIN_API_KEY}" \
-d '{"quoteId":4}'Delete a quote:
curl -X DELETE http://localhost:5269/api/quotes/4 \
-H "X-HappyQOTD-Key: ${HAPPYQOTD_ADMIN_API_KEY}"When running in the Development environment, the app maps the generated OpenAPI document through ASP.NET Core OpenAPI.
The TCP server sends the current quote and closes the connection. The UDP server ignores the contents of each received datagram and replies with the current quote.
Query the local TCP server:
nc 127.0.0.1 17Query the local UDP server:
printf 'hello\n' | nc -u -w2 127.0.0.1 17Query my live deployment over TCP:
nc qotd.kgivler.com 17Or over UDP:
printf 'hello\n' | nc -u -w2 qotd.kgivler.com 17Warning
This instance uses my personal, unfiltered quote collection. Most quotes are programming-related, but some contain strong or obscene language or discuss suicide and other sensitive subjects.
Both listeners use QOTD:ListenAddress and QOTD:Port. Set QOTD:ListenAddress to :: and QOTD:DualMode to true for an IPv4/IPv6 dual-stack deployment. Binding to port 17 may require NET_BIND_SERVICE or elevated privileges on Linux.
Set QOTD:TruncateQuoteResponses to true with QOTD:MaximumQuoteResponseCharacters set to 512 for RFC-style response lengths. Truncation can be disabled to return the complete formatted quote.
The quote of the day is keyed by the current UTC date. The repository selects and stores a quote when that date has no existing selection. Subsequent HTTP, TCP, and UDP requests for that date return the stored selection, including after restarts.
The main configuration sections are:
| Setting | Description |
|---|---|
QotdSecurity:AdminApiKey |
API key required by quote write endpoints. |
QOTD:ListenAddress |
TCP and UDP listener address. Use :: for IPv6 or dual-stack binding. |
QOTD:Port |
TCP and UDP listener port. Defaults to 17. |
QOTD:DualMode |
Enables IPv4 and IPv6 on an IPv6 listener. Use with ListenAddress set to ::. |
QOTD:EnableTcpServer |
Enables the shared hosted TCP server. |
QOTD:EnableUdpServer |
Enables the UDP server. |
QOTD:TruncateQuoteResponses |
Truncates TCP and UDP responses when enabled. |
QOTD:MaximumQuoteResponseCharacters |
Maximum response length when truncation is enabled. Defaults to 512. |
QOTD:ApiBaseUrl |
Configured HTTP API base URL for integrations. |
QOTD:MaxConcurrentConnections |
Maximum simultaneous TCP connections. |
QOTD:RequestTimeoutSeconds |
Configured request-timeout value reserved for the TCP service. |
QOTD:TelemetryIgnoredRemoteAddresses |
TCP and UDP client addresses excluded from served-quote telemetry. |
MissionControl:Enabled |
Enables Mission Control telemetry. |
MissionControl:BaseUrl |
Mission Control Gateway base URL. |
MissionControl:ApiKey |
Mission Control source API key. |
MissionControl:TimeoutMilliseconds |
Mission Control request timeout. |
Environment variables use double underscores, for example:
QotdSecurity__AdminApiKey
QOTD__ListenAddress
QOTD__Port
QOTD__DualMode
QOTD__EnableTcpServer
QOTD__EnableUdpServer
QOTD__TruncateQuoteResponses
QOTD__MaximumQuoteResponseCharacters
QOTD__ApiBaseUrl
QOTD__MaxConcurrentConnections
QOTD__RequestTimeoutSeconds
QOTD__TelemetryIgnoredRemoteAddresses__0
MissionControl__Enabled
MissionControl__BaseUrl
MissionControl__ApiKey
MissionControl__TimeoutMilliseconds
Mission Control is disabled by default in appsettings.json. When enabled, its API key must match a configured Gateway event source and be at least 32 characters long.
Requirements:
- .NET 10 SDK
- Local JoyfulReaperLib packages available through
NuGet.config
Restore and run:
dotnet restore
dotnet run --project HappyQOTD/HappyQOTD.csprojThe Development launch profile uses http://localhost:5269 and enables the Development environment. Change QOTD:Port to a non-privileged port, such as 1717, when port 17 cannot be bound.
Build the project:
dotnet build HappyQOTD/HappyQOTD.csprojBuild the production image:
docker build -t happyqotd .The VPS deployment uses host networking for HappyQOTD. Its relevant settings are:
network_mode: host
environment:
ASPNETCORE_URLS: http://127.0.0.1:5193
QOTD__ListenAddress: "::"
QOTD__Port: 17
QOTD__DualMode: "true"
QOTD__EnableTcpServer: "true"
QOTD__EnableUdpServer: "true"
QOTD__TruncateQuoteResponses: "false"
QOTD__MaximumQuoteResponseCharacters: 512
MissionControl__Enabled: "true"
MissionControl__BaseUrl: http://127.0.0.1:5190
depends_on:
gateway:
condition: service_healthyWith host networking, Docker ports mappings are not used. The host and provider firewalls must allow both TCP port 17 and UDP port 17.
The Gateway health check is /health/ready, which waits for its RabbitMQ connection and channel to be open. Use docker compose up -d --build after changing the image or application code. Use --force-recreate when container configuration or environment variables changed and the existing containers need to be replaced.
The container also needs a writable /app/Data volume for SQLite and NET_BIND_SERVICE when binding port 17:
volumes:
- /var/lib/happyqotd/data:/app/Data
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICEWhen enabled, HappyQOTD publishes these event types:
happyqotd.service.startedhappyqotd.qotd.servedhappyqotd.api.qotd.servedhappyqotd.api.quote.addedhappyqotd.api.quotes.batch_addedhappyqotd.api.randomquote.servedhappyqotd.api.quote.deleted
The happyqotd.qotd.served payload includes a protocol field with values such as tcp and udp, allowing requests to be distinguished by transport.
Telemetry failures are logged and do not normally prevent the HTTP API, TCP server, or UDP server from serving quotes. The startup event is attempted once when the QOTD service starts; restarting HappyQOTD retries it.
Program.csbootstraps the application.Extensions/HappyQotdApplicationExtensions.csregisters services and middleware.Routes/HappyQotdRouteExtensions.csmaps the HTTP endpoints and handlers.QOTDConnectionHandler.cshandles connections from the shared hosted TCP server.QotdUdpServer.cshandles UDP datagrams.Data/QuoteDatabase.csinitializes the SQLite database.Quotes/SqliteRepository.csimplements quote persistence.Events/QOTDJsonContext.csprovides source-generated JSON metadata.
HappyQOTD is licensed under the MIT License.
Copyright 2026 Kyle Givler.