Phoenix 1.7-rc0 Project Organization; Multi-Endpoint & UI

Hi–

So my project has reached a point where how users will interact with the application is important and so this week I’ve started to jump into Phoenix with a more serious eye than that required to get through the typical tutorials & training courses you find out and about.

Phoenix comes with its own opinionated views of how applications are organized as encouraged via its scaffolding and generators. I think this is a good thing in general and fine, as-is, for a lot of applications. Nonetheless, I’ve got a few requirements in my application that have me thinking about this more deeply and thinking I might need to do something at least a bit different. There are a few requirements that are motivating my questions here. My application needs a user interface for normal users logging in from wherever they might be (within some limits)… call this the Public UI. I also need an administrative user interface to manage and monitor the system operations which should not be trivially accessible from just anywhere, this is the Admin UI. Finally, and this is the odd bit, the application should be capable of being run from a single node for… reasons. This suggests that solutions where a phoenix application ‘A’ provides the Public UI and a different phoenix application ‘B’ does the admin interface start to look less good for what I’m trying to build. And my sense it that’s not necessary; though I am happy to be corrected.

At this point I’ve managed to get multiple endpoints for each of the UI types working and I can see I’ll probably also need/want individual routers for the Public UI & Admin UI. This part of dealing with Phoenix feels pretty straightforward and I’ve not really run into anything difficult to grasp (famous last words)… I’m seeing how things are pieced together and I know I’m not exactly treading new territory; that the flexibility to do the things I’m trying to do exists is not surprising. And one last bit of context, my questions below are not really dealing with organizing business logic or database concerns so much; that I’ve got significant progress on outside of what will be the Phoenix implementation: My Phoenix project is really just the interface for users and to provide an API to other systems. With that background… some questions.

My Admin UI I expect to have a modest footprint, but the Public UI has the potential to get complex. As I’m looking at the different modules I’m duplicating, or am likely to duplicate, in some fashion for the different UIs, I’m coming around to the idea that maybe instead of having a myapp_web directory which contains files supporting both UIs kinda thrown together I should split that out into two directory hierarchies a myapp_public_web and myapp_admin_webeach containing the necessary files/modules/etc required to operate each UI. Outside of some possible duplication are there any downsides to doing that? Is there some better way to organize that which I’m not thinking of? Does this hose the ability of Phoenix to be upgraded over time? I could see a third directory in there myapp_common_web for bits that could be shared between the two projects (like maybe the gettext stuff), and I could see where assets have the potential to be tricky in all this, though right now I don’t see an issue with assets. With the above my thinking is that Phoenix likely technically will be fine and that at least I’ll be organizing things in the spirit of Phoenix’s own scaffolded code.

I appreciate that I’m asking more philosophical questions than technical questions, but I guess what I’m really trying to do is get a second opinion before I get too far down a bad path that I can’t see for want of Phoenix experience.

Thanks!
Steve

6 Likes

I put the admin UI pages and components in app_web/live/admin or app_web/dead/admin, common components live in app_web/components and control access with a live_session or Plug pipeline. But I only use the one endpoint.

You could do

app_web/
  admin/
    live/
    dead/
    components/
  public/
    live/
    dead/
    components/

Separate web folders makes sense if they’re separate BEAM applications.

5 Likes

Many thanks for the reply. What you’ve suggested is an absolutely reasonable approach.

Originally I figured I’d be doing something very much like you’re suggesting, however this comment is the distinction that pushes me to have mulitple “web” root directories:

With a single endpoint, the files at the root of app_web directory don’t really change that much; sure the routes are going to reflect how you access the “admin” or “public” side and maybe a couple of other tweaks, but the Endpoint module, the Router module it’s coded to look at, etc. can all fundamentally be as they’re scaffolded by mix phx.new (everything else being equal). But multiple Endpoint modules push the Admin/Public distinction down to the level where endpoint.ex, router.ex, etc. exist. So I’m going to have Admin Endpoint and a Public Endpoint modules, for that distinction to be useful I’m going to need an Admin Router module and a Public Router module, and my hunch is that the Telemetry module will also have some differences (telemetry is a problem for far future me). As the Admin/Public distinction leaks into these bottom level files in app_web, it starts to raise the question if app_web is fundamental or if we should bring the Admin/Public distinction there, too.

All of that is style points stuff though, more important to the humans reading the code than to the technical operations of the software. Your reply is helpful to me because it shows me there is precedence for treating the out-of-box scaffolded directory structure as something that can be futzed with as needed; the biggest concern I have is whether a style related choice is going to break technical requirement (or established conventions); I know there are some naming expectations between certain files and related directories… and I don’t know how deep that might go or how it manifests elsewhere so I get nervous :-).

Thanks again! It was helpful.

1 Like

I’ve found umbrella projects streamline doing things like this quite a bit. You wind up with a directory structure like this:

apps/
  admin_web/ - standard Phoenix stuff for the admin portal. Has its own asset pipeline, etc
  public_web/ - standard Phoenix stuff for the public portal. Has its own asset pipeline, etc
  shared_web/ - maybe? If there are common function components or similar
  data/ - pick a better name for this, but it's contexts / schemas / etc 

The one minor hassle with this approach is that the two Endpoints expect to run on different ports; you can either use something like main_proxy to route traffic to them from one listener, or you could handle that in the load-balancer / SSL-termination layer. The latter is particularly handy if you want to impose rules like “only users on the company VPN can access admin_web”.

3 Likes

For my use case, this is actually the primary motivation for using multiple endpoints. The Admin UI will be security sensitive and so if I can make a firewall the first barrier to unwanted Admin UI attention all the better; I actually don’t know what the final runtime environment will be, but binding the Admin UI to only a non-public IP address wouldn’t be unwelcome if the situation permitted. :slight_smile: If my requirements were a little different, much of the administrative access I’m planning would be in a separate application on its own node, well away from anything directly facing the public internet… oh well.

I do like the umbrella idea as it produces a Phoenix application which looks like the “common model” that gets scaffolded; I just finished setting up an initial environment using my first idea and while I’ve got it up and running I can see where, from a maintenance perspective, it might not be ideal. Given how much stuff will need to be consciously aligned according to UI silo; this is really a problem of needing to remember more “names” of things and being sure the right names are used in the right places. The umbrella method seems like it would quiet that cognitive load since each application in the umbrella would effectively be independent (and largely use the same names for the same things).

Thanks for the ideas!

1 Like

An umbrella project will be your best option here and it’s what I use in a similar situation. For all the “bad press” umbrella projects seem to get, this is one place they can really shine IMO.

If you decide that you want to run the admin app on its own node, mix release is your friend here. Have a look at the Umbrellas section for instructions on how to build a separate release file for whichever app (or combination of apps) you want. Just make sure that the deps for each app are correct so that each one can run entirely on it’s own. The compiler will yell at you and print a warning when it sees that you are trying to use dependencies in an app that aren’t set in that app’s mix.exs.

That way you can have one common project directory to develop and test in with each app running simultaneously with their endpoints configured to run on separate ports that can be started with iex -S mix phx.server at the root of the umbrella project.

You can use mix phx.new [name] --umbrella which will create the base Phoenix umbrella project structure to get you started.

Profit! :sunglasses:

1 Like

Thanks for your input. Actually already landed on that conclusion for many of the reasons you mention.

Having said that, I’ve gone through a few different models of having multiple sites served from a single Elixir deployment and I’m a bit surprised that the most finicky approach to get working correctly is the umbrella approach. At least as far as getting to a simple boilerplate site up and running on each endpoint. Prior to the umbrella, I’ve gotten the dual sites going with a single Phoenix application with multiple Endpoint, Router, etc. modules and I’ve actually tried an approach of creating two standalone Phoenix applications and then adding them as dependencies to a top level Elixir project… sorta “poncho application” style and that was pretty straightforward to get going (that style matches some of the broader application development happening in this project)… and had a lower cognitive load compared the single Phoenix application approach.

However, I’ve had more trouble getting multiple Phoenix apps in a umbrella working well. I think the difference in experience is largely due to wanting to rely more on the Phoenix generators to do the right thing (at least initially) vs. the fact I had to hand-wire the configuration and setup for the other two methods I’ve played with. The generators almost do the right thing when setting up multiple Phoenix apps in an umbrella, but not quite and there’s enough magic in Phoenix that figuring out where something isn’t quite right isn’t as obvious as when I was hand-configuring the other options.

For example the generators will create the umbrella config.exs & dev.exs (& others) in the umbrella and it does create a boilerplate configuration for each of the Phoenix apps as you might expect. But it seems to fail to handle the esbuild/tailwind part of that configuration correctly for that scenario; thanks to a helpful post by @ Kurisu / Tailwind + Esbuild for umbrela applications - #3 by Kurisu I was able to get past that issue though there are other issues still present stopping the basic Phoenix starter pages to pop up; I expect they’ll be similar to the configuration issue above and not too terrible to track down, but clearly the generators only go so far. (And yes, I’m using release candidate Phoenix so that could well be a factor and understood as compromising action).

In the end I do still think the umbrella is the correct approach for dealing with the multiple UI management. Once the generated configurations are sorted I’m expecting things to work on par with the first approaches I tried with the added advantage of some of the tooling that is umbrella aware.

Thanks again!

I didn’t read this whole thread, but I’m tipsy and wanna throw in some opinions… :smiley:

We have multiple apps running off a single endpoint. By that, I mean multiple domains, cookies, session stores, routers etc.

Our endpoint doesn’t end with plug OurApp.Router, but instead a simple plug that examines headers, paths, etc etc, then forwards to a normal Phoenix router.

We use runtime configuration to setup the different sessions.

I don’t recommend doing all this; we’re considering just having multiple endpoints… :joy:

We did start as an umbrella app and went back to non-umbrella with just a bunch of top level namespaces, which I think is a lot more simple.

So you have many apps, each exposing a Phoenix Endpoint, but you want all traffic to be served from the same port. This is the master proxy pattern.

I have an example here: GitHub - evadne/snake: 🐍 Multi-Snake 🐍 — A showcase of Elixir clustering capabilities and strategies.snake/config.exs at master · evadne/snake · GitHub shows 1 endpoint at root but you can add more. Obviously LiveView works meaning WebSocket works.

As to having many web apps.

You can have assets in the core web app and then all apps inherit. Same for layouts, etc. But don’t go overboard. Sometimes copy & paste is fine. If you have 2 web apps and you have a lot of dependencies then you should probably merge them.

1 Like

I’m curious why you don’t recommend single endpoint with multiple routers. Could you expand a little bit more?

I think it’s more flexible; you can configure each endpoint individually if you need to. Plus I think it’s closer to vanilla Phoenix.

I have to explain our single endpoint with a “super router” setup to new devs, and it always feels icky.

We did end up breaking out one of our apps into its own separate endpoint because we needed to configure its session differently. So now we have two endpoints, but they both forward requests to our “super router”, which then decides which normal Phoenix router to forward to.

I think our original motivation for a single endpoint was something silly like “we don’t want to startup servers on multiple ports.”

1 Like