0

I'm working on a system with a user-facing frontend and with 1-n backend services which I'm trying to design according to the principles of the Twelve-Factor App.

I'm now facing the task of sending emails to the user that contain a link to the frontend. The question is, how do I generate the URLs, or more specifically where do I take the URL domain/host from?

I'm seeing the following approaches:

1. Put the domain in the configuration

I'm putting the domain in an environment variable and expose it to the service using the framework's configuration API.

Here, I see a potential problem if in the future the application needs to be available from multiple domains (e.g. one domain per country or multi tenancy). Currently, this is not the case, but who knows what new requirements might come.

2. Extract the host from the request

When the the user enables email notifications, I extract the domain from the request headers. As the email(s) will be sent at a later time, I'll need to persist the domain in the database where the rest of the per-user settings live.

This will work in a scenario where the application has more than one domain but until this is the case, putting the same value (domain = "https://my-domain.com") in every database entry feels redundant.

3. Have the frontend generate the whole URL

Currently, the system only serves one domain but it already supports localization using subpaths (think /{language}/login). To generate the correct URL for each user, I need to persist the locale that will be part of the URL in the DB anyway.

This begs the question, why not have the frontend generate the complete URL including domain, language and path and store it in the database? This would kill two birds with one stone since now the backend doesn't need to know the URL structure of the frontend. However this would potentially open up a possibility for a malicious client to mess with the generated URLs. The redundancy argument from 2. also applies.

Are there any more arguments for/against the given approaches or even alternative approaches or does it come down to taste?

2 Answers 2

2

You want to achieve a separation of concerns between the app and hosting the app.

This is why you have the "config in environment" rule.

Here though you have a clear dependency between the app generating the email with a url that works and the ability to host the app anywhere without changing the code.

Clearly The solution most inline with the rules is to put the url in the config with the other variables. Your multiple domain objection is solved by having a config per domain.

Having the app reflect upon its hosting is fraught with risk and creates the very dependency you are trying to avoid.

If the problem becomes even more complex, say dynamic setup of new tenants, or configuration of email templates. Then you have an argument that this field is no longer configuration, but simply part of the application state which can be persisted on a database and setup through a management interface.

This maintains the app/hosting separation and the cleanliness of the config, allowing it to remain part to the CI/CD rather than being polluted with application state.

1

I'd rule out #3 for exactly the reason that you stated: it poses a high risk for XSRF. Say i trigger a password reset for you and set the link for the set-a-new-password page to https://example.com/users/me/delete.

If you want to avoid storing the domain with every process/row in the database you're left with #1.

But i've seen #2 implemented in the past quite successfully, especially with HATEOAS. There almost always is a reverse-proxy or an API Gateway between your end-users and your actual application instance, and that middleman will have to pass the domain on. The standard-practice is to use the X-Forwarded-* headers, especially X-Forwarded-Host and X-Forwarded-Proto. Almost all good reverse-proxies can be easily configured to pass these on. This won't save you from coupling the URL-structure of your frontend to your backend, unfortunately.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.