Skip to content

Repository files navigation

Shortly

License: MIT .NET 10 Dart 3.12 Database: SQLite WAL MCP Ready Ask DeepWiki Docs maintained with Skill Steward

A small, legible, and agent-operable URL shortener built with .NET 10 (Minimal API) and a Dart local steward (CLI + MCP).

Shortly prioritises correctness, observability, and deterministic lifecycle over premature scale. It features a single shared pool of links, soft TTL expiration, check-on-read validation, and complete audit history for every redirect attempt (both successes and failures).

See story why it was created and how on dev.to.

To understand principles of Engineering Stewardship and Skill Steward which were used to scaffold and work with entire project see repository GitHub.com/arenukvern/skill_steward.


πŸ“‘ Table of Contents


🎯 Core Principles & Architecture

  • Single Shared Pool: Every shorten request generates a new short code even if the target URL has been shortened before.
  • Pure Encoder: Base62 encoding is purely mathematical and decoupled from storage.
  • Soft TTL & Check-on-Read: No aggressive background deletion jobs. Expiration is evaluated dynamically upon lookup.
    • Active links return 302 Found.
    • Expired links return 410 Gone with a friendly HTML status page.
    • Non-existent or soft-deleted links return 404 Not Found.
  • Full Attempt Audit Trail: Every redirect attempt is recorded with timestamp, IP, User-Agent, and failure reason (including attempts for unknown codes).
  • SQLite + WAL Mode: High-performance single-file persistence with Write-Ahead Logging.
  • Standalone Runtime vs. Developer Steward: The .NET runtime has zero dependencies on Node, Dart, or Docker. Dart and Node are strictly part of the developer/stewardship toolchain (ADR 0009).

πŸ› Locked Architectural Decisions

Architectural decisions are formally recorded and immutable without an explicit charter revision:

ADR Title Summary
ADR 0001 Single Shared Pool Global namespace; identical URLs yield unique codes.
ADR 0002 Public Shorten, Admin Key Public shortening and redirection; mutating/inspecting requires X-Admin-Key.
ADR 0003 Pure Base-N Encoding Sequential numeric identity mapped deterministically to Base62 alphanumeric codes.
ADR 0004 Soft Expiry & Check-on-Read In-band expiry calculation returning 410 Gone without background sweeps.
ADR 0005 SQLite with WAL Embedded storage with WAL journal mode for concurrent reads.
ADR 0006 Configurable Host, Alphabet & TTL Environment-driven settings with safe fallback defaults.
ADR 0007 Dart Stewardship & Tooling Dart CLI and MCP tool surface for local management.
ADR 0008 Testable Without Docker Standard testing and local execution run natively without containers.
ADR 0009 Local Stewardship Stack Unified just + ./install.sh + Skill Steward + GitNexus stack.

πŸ’» Prerequisites

Runtime Path (Minimal)

  • .NET 10 SDK (runs the core web service and unit/integration tests).

Developer & Steward Path (Recommended)


πŸš€ Quick Start from Scratch

1. Clone & Bootstrap

Clone the repository and run the automated bootstrap script:

git clone https://github.com/arenukvern/shortly.git
cd shortly

# macOS / Linux
./install.sh

# Windows PowerShell
.\install.ps1

./install.sh checks for prerequisites, installs missing components (just, .NET 10, Dart, Node, GitNexus), and initialises local agent skills.

To inspect tooling status without making modifications:

just status
# or: ./install.sh --status

2. Run Quality Checks

Verify that the local environment, test suites, and steward contracts pass:

just check

(Executes dotnet test and shortly steward validate).

3. Start the API Service

Launch the service locally:

just run

The service starts at http://localhost:5080.

4. Run with Docker (Optional)

To run in a containerised environment:

just docker-up

The Docker endpoint is accessible at http://localhost:8080.


πŸ“‘ API Reference

Public Endpoints

1. Shorten a URL

  • Method: POST /api/v1/shorten
  • Request Body:
{
  "originalUrl": "https://example.com/very/long/url?param=1",
  "customAlias": "my-alias", 
  "ttlDays": 30
}

(Note: customAlias and ttlDays are optional).

  • Response (201 Created):
{
  "code": "my-alias",
  "shortUrl": "http://localhost:5080/my-alias",
  "originalUrl": "https://example.com/very/long/url?param=1",
  "expiresAt": "2026-09-14T12:00:00Z",
  "createdAt": "2026-08-15T12:00:00Z"
}
# Example curl:
curl -s -X POST 'http://localhost:5080/api/v1/shorten' \
  -H 'Content-Type: application/json' \
  -d '{"originalUrl": "https://xsoulspace.dev"}'

2. Follow a Short Code (Redirect)

  • Method: GET /{code}
  • Responses:
    • 302 Found with Location: <originalUrl> (Active link).
    • 410 Gone with HTML status page (Expired link).
    • 404 Not Found (Unknown or soft-deleted link).
# Inspect redirect headers without following:
curl -sI 'http://localhost:5080/my-alias'

# Follow redirect in terminal:
curl -L 'http://localhost:5080/my-alias'

3. Health & Expiration Pages

  • GET /health β†’ {"status": "ok"}
  • GET /expired?code=abc β†’ HTML status page explaining the link has expired.

Administrative Endpoints (Requires X-Admin-Key)

All administrative routes require the X-Admin-Key header (local default: change-me-in-production).

1. Retrieve Link Metadata

curl -s 'http://localhost:5080/api/v1/links/my-alias' \
  -H 'X-Admin-Key: change-me-in-production'

2. List Links (Paginated)

curl -s 'http://localhost:5080/api/v1/links?offset=0&limit=20' \
  -H 'X-Admin-Key: change-me-in-production'

3. Update Link Destination or TTL

curl -s -X PUT 'http://localhost:5080/api/v1/links/my-alias' \
  -H 'Content-Type: application/json' \
  -H 'X-Admin-Key: change-me-in-production' \
  -d '{
    "originalUrl": "https://example.com/updated-path",
    "expiresAt": "2026-12-31T23:59:59Z"
  }'

4. Soft Delete Link

curl -s -X DELETE 'http://localhost:5080/api/v1/links/my-alias' \
  -H 'X-Admin-Key: change-me-in-production'

5. Audit Attempts Log

curl -s 'http://localhost:5080/api/v1/attempts?code=my-alias&limit=50' \
  -H 'X-Admin-Key: change-me-in-production'

6. Pool-Wide Statistics

curl -s 'http://localhost:5080/api/v1/stats' \
  -H 'X-Admin-Key: change-me-in-production'

πŸ›  Dart Local Steward & MCP Server

Shortly includes a Dart-powered steward CLI and Model Context Protocol (MCP) server under tools/shortly_dart/.

CLI Commands

cd tools/shortly_dart

# Show help
dart run shortly --help

# Shorten a link
dart run shortly shorten https://example.com --base-url http://localhost:5080

# Inspect link information (requires admin key)
SHORTLY_ADMIN_KEY=change-me-in-production dart run shortly info my-alias --base-url http://localhost:5080

# Fetch pool stats
SHORTLY_ADMIN_KEY=change-me-in-production dart run shortly stats --base-url http://localhost:5080

# Run repository steward checks
dart run shortly steward validate

Model Context Protocol (MCP)

To connect Shortly to AI assistants (Zed, Claude Code, Cursor, Windsurf), run the steward in MCP mode:

dart run shortly mcp

Exposed MCP Tools:

  • shortly_shorten
  • shortly_get_link_info
  • shortly_list_links
  • shortly_list_attempts
  • shortly_get_stats
  • shortly_update_link
  • shortly_delete_link
  • shortly_steward_validate
  • shortly_steward_status

βš™οΈ Configuration & Environment Variables

Settings are defined in src/Shortly.Api/appsettings.json and can be overridden via environment variables:

Setting Environment Variable Default Description
Shortly:BaseUrl Shortly__BaseUrl https://localhost:5001 Public base URL used when returning shortened links.
Shortly:AdminApiKey Shortly__AdminApiKey change-me-in-production Secret key required for /api/v1 management endpoints.
Shortly:ConnectionString Shortly__ConnectionString Data Source=shortly.db SQLite database connection string.
Shortly:DefaultTtlDays Shortly__DefaultTtlDays 30 Default expiration window in days.
Shortly:DefaultCodeLength Shortly__DefaultCodeLength 7 Length for auto-generated sequential codes.
Shortly:AllowCustomAliases Shortly__AllowCustomAliases true Whether custom aliases are enabled.
Shortly:CustomAliasMinLength Shortly__CustomAliasMinLength 3 Minimum character length for custom aliases.
Shortly:CustomAliasMaxLength Shortly__CustomAliasMaxLength 32 Maximum character length for custom aliases.

⚠️ Production Note: Always override Shortly__AdminApiKey in production deployments. just validate will enforce that the default key is not used in production configurations.


⏱ Benchmarking

Shortly includes a local redirect timing tool in tools/Shortly.Bench/ to measure redirect latency and compare internal database lookups against external destination hops:

# Run standard in-process benchmark:
just bench-redirect

# Run with 300 iterations:
just bench-redirect -- --iterations 300

# Benchmark a live instance and follow the destination hop:
just bench-redirect -- --base-url http://localhost:5080 --code my-alias --follow

πŸ“‚ Repository Structure

.
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ Shortly.Core/             # Pure domain models, Base62 encoder, validators
β”‚   └── Shortly.Api/              # Minimal API host, SQLite EF Core, endpoints, middleware
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ Shortly.Core.Tests/       # Unit tests for domain logic and encoding
β”‚   └── Shortly.Api.Tests/        # Integration and E2E tests for API routes
β”œβ”€β”€ tools/
β”‚   β”œβ”€β”€ Shortly.Bench/            # Benchmark harness for redirect performance
β”‚   β”œβ”€β”€ shortly_dart/             # Dart CLI and MCP steward implementation
β”‚   └── *.sh / *.ps1              # Shell and PowerShell cross-platform tool scripts
β”œβ”€β”€ docs/
β”‚   β”œβ”€β”€ NORTH_STAR.mdx            # Project charter, non-goals, and vision
β”‚   β”œβ”€β”€ DESIGN_FAQ.mdx            # Architectural rationale (why)
β”‚   β”œβ”€β”€ DX_FAQ.mdx                # Operational guide & troubleshooting (how)
β”‚   β”œβ”€β”€ decisions/                # Architecture Decision Records (ADRs 0001–0009)
β”‚   └── articles/                 # Showcase story & documentation
β”œβ”€β”€ .agents/skills/               # Agent skills for repository maintenance
β”œβ”€β”€ AGENTS.md                     # Agent map and repository instructions
β”œβ”€β”€ justfile                      # Command hub recipes
β”œβ”€β”€ install.sh / install.ps1      # Toolchain bootstrap scripts
└── LICENSE                       # MIT License

🀝 Contributing & Governance

Contributions that respect the project's North Star and locked architectural decisions are welcome!

  1. Check Decisions First: Before proposing structural changes, consult docs/decisions/ and docs/DESIGN_FAQ.mdx.
  2. Adhere to Native Gates: Ensure all changes pass just check (dotnet test and shortly steward validate).
  3. No Unwanted Dependencies: The core service must remain runnable with only the .NET 10 SDK.
  4. Code Intelligence: Refresh GitNexus with npx gitnexus analyze after making significant refactors.

πŸ“„ License

This project is licensed under the MIT License.

About

links shortener - dot net based - showcase for skill steward

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages