How to structure a Phoenix umbrella framework for many applications
Asked Answered
T

2

11

I'm playing around with an architecture for a new suite of products using phx 1.3 and umbrella apps.

I have an existing Phoenix based enterprise class WebRTC soft phone (many keys, a display, multiple input and output audio device selection, and more). I have developed a Slack clone messaging app prototype with Phoenix. Both applications are fairly large I need to integrate the phone with the chat app into one front end that may be either just the phone, just the chat client, and both. I will need to add a lot of new features to the chat client moving forward I also want the architecture to support using the same client to provision additional setting on the call server (user based) and potentially a large number of admin level settings. I may also be adding other applications in the future like an operator panel, log viewer, and the list goes on... The client side JS is pretty simple, no front end framework. I render templates server side and push the html out over channels.

I would like to build this pluggable. Same endpoint and database. One common UX.

I think there will be two common apps in the umbrella, one for the Phoenix endpoint and a couple controllers and another for the main Repo and a couple schemas. I'm trying to figure out how difficult it will be to use two or more additional apps for each application. One for context and schema, another for controllers, views, templates, and brunch resource. Possibly another for 3rd party APIs.

To make this work, I'll need a dynamic dispatch for routers in each of the apps. A method for handling migrations contained in each of the apps, and probably more that I have not thought of yet.

As anyone tried this? Are there any open source projects with a similar structure?

Tsuda answered 19/4, 2017 at 9:22 Comment(0)
E
14

The elixir app I work with day-to-day is an umbrella with 13 apps.

The at the root is the Phoenix endpoint and top level Router which forwards requests to Routers defined in the other apps.

This means the app is not split into layers (web/business/data) but rather into vertical domain slices.

This has scaled well as the app has grown significantly over the last 12 months.

The biggest gotcha I've had is that Phoenix routers strip the leading path from the request when forwarding to other routers, so we created a mount macro to use with the Plug router which keeps the request path unchanged:

defmodule MyApp.Router do
  @moduledoc """
  Top level routing to each of the sub-systems
  """

  use Plug.Router

  plug :match
  plug :dispatch

  mount "/events/*_", Events.Router
  mount "/report/*_", Report.Router
  mount "/stats/*_",  Stats.Router
  mount "/auth/*_",   Auth.Router
end

and mount:

defmacro mount(path, router, opts \\ []) do
  quote do
    @opts unquote(router).init(unquote(opts))
    match unquote(path), do: unquote(router).call(var!(conn), @opts)
  end
end

We manage the migrations of the entire database in a single app just for simplicity, but Ecto Schemas are declared in each app separately.

Here is a project that demonstrates some of the concepts.

Estrange answered 20/4, 2017 at 22:25 Comment(6)
Thanks! I like your mount approach. I had this issue in the past. Did not think of your approach. However, I tried a quick spike with this ` Application.get_env(:app, :routers) |> Enum.map(&(forward "/", &1))`. I put that at the end of my main router and it seems to work. Can you think of any issues with that approach?Tsuda
vertical domain slices sounds nice. I have seen references to extracting the persistence/db into its own app apps/db/. What are your thoughts on this?Partlet
I see you mention that We manage the migrations of the entire database in a single app just for simplicity. Have there been any issues with this architecture?Partlet
So far the db app has been fine. It is a reflection of the architecture where a number of umbrella apps share a single database, with multiple postgresql schemas for logical separation. The shared app only needs to declare the Repo module and the migration scripts. Each of the other umbrella apps are free to manage their own Ecto.Schema modules and query builders.Estrange
Hi @MikeBuhot I Loved your solution. But I am wondering is there a way to share channel between the apps I have below structure: apps --proxy_app ----endpoint.ex ----router_plug.ex(Web.Plugs.Router) ----user_socket.ex ----channels ------room_channel.ex --web_app_1 ----router.ex --web_app_2 ----router.ex -- proxy_app includes web_app_1 and web_app_2. Sometimes I might need to broadcast events from web_app_1 and web_app_2. is there a way I can achieve that? Thanks in advanceChampollion
@Champollion If possible put the channel module in web_app_1 or web_app_2. If you need to broadcast from a channel message handler, you can get the endpoint/pubsub_server from the socket: hexdocs.pm/phoenix/Phoenix.Socket.html#module-socket-fieldsEstrange
M
0

I'm also working on quite a large umbrella application. Instead of maintaining a file with configured entry points to each app I wanted to see if I can make it more dynamic. So I wrote a plug that looks like this:

def call(%{path_info: [path|_]} = conn, opts) do
  path = path |> String.downcase() |> Macro.camelize()
  module =
    try do
      Module.safe_concat [LocationRouting, path, Location]
    rescue
      _ -> nil
    end
  if module do
    module.call(conn, opts)
  else
    conn
  end
end

I added that plug to the endpoint. And then it's up to the called module which is again capable of adding a router or something similar to an endpoint by using Plug.Builder

You can look at the full example here: https://github.com/tverlaan/location_routing

Mascle answered 12/12, 2017 at 18:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.