Rails Dynamic Links
This Rails app is an alternative to Firebase Dynamic Links, aiming for 100% compatibility. It provides a self-hosted URL shortener and dynamic link service.
Core functionality is provided by the dynamic_links
gem, a Rails engine included in this app.
Features (via dynamic_links
gem)
- Multiple URL shortening strategies: MD5 (default), NanoId, RedisCounter, Sha256, and more
- Consistent or unique short links depending on strategy
- Fallback mode: Optionally redirect to Firebase Dynamic Links if a short link is not found
- REST API for programmatic access (can be enabled/disabled)
- Redis support for advanced strategies
- Import/export for Firebase Dynamic Links data
- Optional performance monitoring with ElasticAPM (disabled by default)
For users migrating from Firebase, download your short links data from https://takeout.google.com/takeout/custom/firebase_dynamic_links and import it on the /import
page.
Project Status
Check out our Project Board to see what's been completed and what's still in development.
Documentation
- ElasticAPM Integration - Performance monitoring setup and usage
Getting Started
Prerequisites
Make sure you have the following installed on your system:
- Ruby 3.4.4
- Node.js 22+
- PostgreSQL
- Redis (optional, required for some strategies)
Development Setup
Using VS Code Dev Containers (Recommended)
This project includes a complete VS Code development container configuration with all dependencies pre-installed.
- Install Visual Studio Code
- Install the Dev Containers extension
- Clone this repository
- Open the project in VS Code
- When prompted, click "Reopen in Container" or run the command
Dev Containers: Reopen in Container
The dev container will automatically:
- Set up Ruby 3.4.4
- Install Node.js 22
- Install all required system dependencies
- Install Ruby gems and Node packages
- Configure VS Code with recommended extensions and settings
Manual Setup
If you prefer to set up the development environment manually:
-
Clone the repository:
git clone https://github.com/saiqulhaq/dynamic_links.git cd dynamic_links
-
Copy environment variables:
cp .env.example .env
-
Install dependencies:
bundle install yarn install
-
Set up the database:
rails db:create rails dynamic_links:install:migrations rails db:migrate
-
Start the development server:
rails server
Visit http://localhost:3000 in your browser.
Usage
Each shortened URL belongs to a client. Create your first client in the Rails console:
DynamicLinks::Client.create!(name: 'Default client', api_key: 'foo', hostname: 'google.com', scheme: 'http')
To shorten a link via the REST API, send a POST request to http://localhost:3000/v1/shortLinks
with this payload:
{
"api_key": "foo",
"url": "https://github.com/rack/rack-attack"
}
The response will look like:
{
"shortLink": "http://google.com/a6LlbtC",
"previewLink": "http://google.com/a6LlbtC?preview=true",
"warning": []
}
Testing
Run the test suite with:
rails test
To run tests with asset building:
yarn build
yarn build:css
rails test
Comprehensive Testing
For testing the entire application including all engines, use the comprehensive test runner:
./bin/test_all
This script will:
- Run tests for the main Rails application
- Run tests for all engines (
dynamic_links
,dynamic_links_analytics
) - Provide colored output with detailed reporting
- Show execution time and summary of results
- Return appropriate exit codes for CI/CD integration
The script automatically detects the appropriate Rails command for each component and handles engine-specific test execution.
Configuration
DynamicLinks Engine Configuration
You can configure the dynamic_links
engine in an initializer (e.g., config/initializers/dynamic_links.rb
). Example:
DynamicLinks.configure do |config|
config.shortening_strategy = :md5 # :md5, :nanoid, :redis_counter, :sha256, etc.
config.redis_config = { host: 'localhost', port: 6379 }
config.redis_pool_size = 10
config.redis_pool_timeout = 3
config.enable_rest_api = true
config.enable_fallback_mode = false # If true, fallback to Firebase if not found
config.firebase_host = "https://example.app.goo.gl" # Used for fallback
config.max_shortened_url_length = 15 # Maximum length for shortened URL tokens (default: 15)
end
What is Fallback Mode?
Fallback Mode allows your Rails app to redirect users to the original Firebase Dynamic Links service if a requested short link is not found in your local database. This is useful when you are migrating from Firebase and want to ensure that any links not yet imported or created in your self-hosted service will still work for end users.
- When
enable_fallback_mode
is set totrue
, and a short link is not found locally, the app will automatically redirect to the URL specified byfirebase_host
(with the same path and query parameters). - When set to
false
, missing links will return a standard 404 error.
This feature helps provide a seamless migration experience from Firebase Dynamic Links to your own self-hosted solution.
See the dynamic_links README for all available options and strategies.
Optional dependencies
- For
:nanoid
strategy: addgem 'nanoid', '~> 2.0'
- For
:redis_counter
strategy: ensure Redis is running and addgem 'connection_pool'
To configure rate limiting, edit config/initializers/rack_attack.rb
. See https://github.com/rack/rack-attack#throttling
Events and Analytics
The dynamic_links
engine publishes Rails instrumentation events for tracking link usage. For detailed information about events, analytics integration, and available analytics engines, see the dynamic_links engine documentation.
Back-end
- PostgreSQL
- Redis (optional, required for some strategies)
- Sidekiq
- Action Cable
- ERB
Front-end
Production Deployment
For production deployment, you'll need to:
- Set up PostgreSQL and Redis servers
- Configure environment variables in
.env
file - Set
RAILS_ENV=production
andNODE_ENV=production
- Run
rails assets:precompile
to build assets - Use a process manager like systemd or supervisor to manage Rails and Sidekiq processes
- Set up a reverse proxy (nginx/Apache) to serve static files and proxy requests
Puma Configuration
The application uses Puma as the web server with intelligent worker configuration:
-
Default behavior: Uses
Etc.nprocessors * 2
workers with app preloading for optimal performance -
Containerized environments: Set
WEB_CONCURRENCY=0
for single-process mode to avoid memory issues and child process warnings in limited memory containers (e.g., Kubernetes with 512Mi memory limit) -
Custom worker count: Set
WEB_CONCURRENCY=N
to use exactly N workers
Container Deployment Recommendations:
- For containers with ≤512Mi memory: Use
WEB_CONCURRENCY=0
(single process) - For containers with ≥1Gi memory: Use
WEB_CONCURRENCY=1
or higher - The configuration automatically handles SIGCHLD signals to prevent "reaped unknown child process" warnings in containerized environments
Development Tools
- Code formatting:
bundle exec rubocop -a
- Linting:
bundle exec rubocop
- Asset building:
yarn build && yarn build:css
Environment Variables
All configuration is managed through environment variables. Copy .env.example
to .env
and adjust the values for your setup. Key variables include:
Core Application
-
DATABASE_URL
- PostgreSQL connection string -
REDIS_URL
- Redis connection string -
SECRET_KEY_BASE
- Rails secret key -
RAILS_ENV
- Environment (development/production)
Production Security & Networking
-
ALLOWED_HOSTS
- Comma-separated list of allowed hostnames for DNS rebinding protection (default:example.com,*.example.com
) -
TRUSTED_PROXIES
- Comma-separated list of CIDR ranges for trusted proxies (default: empty, configure for Kubernetes/Docker networks) -
RACK_ATTACK_TRUSTED_IPS
- Comma-separated list of IP addresses with higher rate limits (default: empty)
Kubernetes/Docker Deployment
For Kubernetes deployment with ConfigMap, configure these variables:
- Set
ALLOWED_HOSTS
to your domain(s), e.g.,myapp.com,*.myapp.com
- Set
TRUSTED_PROXIES
to your cluster's CIDR ranges, e.g.,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
- Optionally set
RACK_ATTACK_TRUSTED_IPS
for specific IPs that need higher rate limits
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request