Skip to content

feat: Configurable short code lengths & progressive generation#214

Open
mfcarroll wants to merge 3 commits intopiffio:mainfrom
mfcarroll:feat/code-length
Open

feat: Configurable short code lengths & progressive generation#214
mfcarroll wants to merge 3 commits intopiffio:mainfrom
mfcarroll:feat/code-length

Conversation

@mfcarroll
Copy link
Copy Markdown
Contributor

@mfcarroll mfcarroll commented Mar 27, 2026

Hi @piffio, this is actually the main piece I was working on. Being able to generate random codes shorter than 6 digits is an essential feature for us, and I think will also be quite desirable for other users. I believe it is implemented here in a way that improves the app overall, centralises the defaults and settings, while maintaining the current status quo and safeguarding speed and resiliency. I realise this is a significant architectural change so I apologise for not discussing before hand. Thank you for clarifying the process and I will be sure to follow that in future. 🙏

Overview

This PR introduces the ability for instance administrators to configure the minimum lengths for both random and custom short codes, replacing the previous hard-coded 6-character and 3-character defaults. It implements a self-healing progressive generation system to ensure that namespaces can scale automatically as they fill up, with essentially no performance impact.

Rationale / Namespace Mathematics

Previously, Rushomon enforced a strict 6-character minimum for generated codes. Using a Base62 alphabet (A-Z, a-z, 0-9), a 6-character code yields 56.8 billion combinations.

While robust, and great for public SaaS platform, this is overkill for many self-hosted, single-tenant, or internal team deployments (i.e. my use case).

  • A 3-character code yields 62³ = 238,328 combinations.
  • A 4-character code yields 62⁴ = 14,776,336 combinations.

For most self-hosted instances, 238k to 14M links is more than sufficient for their lifespan. This feature allows administrators to configure shorter, more aesthetic URLs out of the box, drastically improving the UX for instances that do not need billions of unique paths, in cases where short urls are typed manually.

Collision Handling & Progressive Scaling

To safely allow smaller namespaces without risking infinite collision loops when the namespace eventually saturates, I've implemented a Progressive Short Code Generator:

  1. When generating a random code, the system attempts to create one at the current effective_min_length.
  2. If it detects 3 consecutive collisions at this length, it assumes the current namespace is effectively exhausted.
  3. It automatically increments the target length by 1 and continues generating.
  4. Crucially, it then updates a system_min_code_length "high-watermark" in the database.

This means the application is completely self-healing. An admin can start their instance at 3 characters or less. Once ~14-17k links are generated, the system will seamlessly upgrade itself to 4 characters, completely invisibly to the user and with zero downtime. (The cost along the way is a few hundred collisions total.)

Power-User Override: For self-hosters who want to stretch their short namespaces and accept the mild performance cost of extra RNG loops, the collision exhaustion trigger can be explicitly overridden via the COLLISION_THRESHOLD environment variable in wrangler.toml or .dev.vars. It seemed best to not include that in the admin UI as for the vast majority of users that would likely cause unnecessary confusion.

Local testing

  1. Set COLLISION_THRESHOLD=1 in your .dev.vars
  2. Go to the Admin Settings and set both the Random and Custom minimum lengths to 1.
  3. Create ~15-20 links. It will fairly quickly hit a single collision and bump the high watermark to 2 across the application, including in the admin UI.

Backend Changes

  • Database & Settings: Added min_random_code_length, min_custom_code_length, and system_min_code_length to the settings table (seeded via a new migration).
  • Constants Centralization: Moved hard-coded length constraints into a dedicated set of constants in src/utils/short_code.rs to ensure a single source of truth across the backend.
  • API Validation: Relaxed utils::validation::validate_short_code to check for absolute system limits (1 to MAX_SHORT_CODE_LENGTH [100]). Runtime minimums are now enforced at the route level.
  • Public Settings Endpoint: The GET /api/settings endpoint now computes and returns the effective minimums, so the frontend doesn't have to duplicate the business logic.
  • Import & Creation: Both the standard creation endpoint and the CSV batch importer strictly enforce the effective custom bounds.

Frontend Changes

  • Constants Centralization: Mirrored the backend architecture by pulling hard-coded lengths into a centralized frontend/src/lib/constants.ts file.
  • Admin Dashboard: Added UI to configure the random and custom length limits. The inputs are physically bounded by the system_min_code_length to prevent an admin from trying to force the system into an already-exhausted namespace.
  • Link Creation (UI): <LinkModal> now consumes the effective minimum lengths passed down from the publicSettings loaded in the dashboard layout, dynamically updating its validation rules and helper text.

The existing defaults have been preserved throughout, but this feature could also be used to allow for shorter defaults as standard, by changing the two default constants files, at the maintainer's discretion.

Note on Orphaned Component

While adding this feature, I noticed that frontend/src/lib/components/CreateLinkForm.svelte is currently an orphaned component (all active link creation is handled via LinkModal.svelte). I have updated it to accept the new minShortCodeLength prop and use the dynamic validation rules. However it appears to lag significantly behind LinkModal.svelts in terms of features and validation so it may be better to simply remove it.

@mfcarroll
Copy link
Copy Markdown
Contributor Author

I realized the progressive generator in router.rs was making separate database calls to fetch each length limit so I pushed an optimization to fetch all settings in a single D1 query instead.

@mfcarroll
Copy link
Copy Markdown
Contributor Author

Last update: In the process of deploying this for my own use I realised I should have added the COLLISION_THRESHOLD variable to the config examples, scripts, and upstream ci/cd for others to reference. That is all added in the latest commit.

- Documented optional COLLISION_THRESHOLD in example config files
- Updated ci/cd scripts to pass vars.COLLISION_THRESHOLD
- Updated common.sh and deployment.sh to pass env.COLLISION_THRESHOLD
@piffio
Copy link
Copy Markdown
Owner

piffio commented Apr 1, 2026

Hey @mfcarroll
I am currently working on wrapping up the next version 0.7.0 and the most important missing piece will be API documentation as well as some further testing.

I'll get back to this proposal after 0.7.0 has been released. I'll introduce frontend linting and formatting at that moment too, which means every single frontend file will be updated.

Expect some merge issues.

Thanks for your patience.

@mfcarroll
Copy link
Copy Markdown
Contributor Author

Sounds good, and no worries I can deal with the merge issues down the road after 0.7.0.

@piffio
Copy link
Copy Markdown
Owner

piffio commented Apr 9, 2026

Hello @mfcarroll,

I've just merged the massive #252, which means there are all sorts of conflicts now due to the changes in the frontend formatting.

The good news is that this will stay consistent and enforced going forward.

The bad news is that you'll need to do some rebasing/merge conflict handling to get this PR back to a mergeable state.
But there won't be any better moment than now since I've just opened the 0.8 merge window.

Thanks again for all your contributions!

PS: I'll review the reported issue on frontend/src/lib/components/CreateLinkForm.svelte on my end, thanks for bringing this up!

@mfcarroll
Copy link
Copy Markdown
Contributor Author

All good. I'll take a look this week sometime and get that rebased.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants