Who We Are

We are Optimum BH - A cutting edge software development agency specializing in Full-stack development with a focus on web and mobile applications built on top of PETAL stack.

What We Do

At Optimum BH, we are dedicated to pushing the boundaries of software development, delivering solutions that empower businesses to thrive in the digital landscape.

Web app development

We create dynamic and user-friendly web applications tailored to meet your specific needs and objectives.

Mobile app development

We design and develop mobile applications that captivate users, delivering an unparalleled experience across iOS and Android platforms.

Maintenance and support

Our commitment doesn't end with deployment. We provide ongoing maintenance and support to ensure your applications remain up-to-date, secure, and optimized for peak performance.

Blog Articles

Post Image

Getting Started with Ash Framework in Elixir

Are you looking for a powerful and flexible way to build Elixir applications? Look no further than the Ash framework! In this blog post, we'll introduce you to Ash, explain why it's great for building applications, and show you how to get started. What is Ash Framework? Ash is a declarative, resource-based framework for building Elixir applications. It provides a set of powerful tools and abstractions that make it easier to build complex, data-driven applications while maintaining clean and maintainable code. Why Use Ash Framework? Declarative Design: Ash allows you to define your application's structure and behavior declaratively, making your code more readable and maintainable. Resource-Based Architecture: With Ash, you model your application around resources, which encapsulate data and behavior in a cohesive way. Built-in Features: Ash provides many built-in features like pagination, filtering, and authorization, reducing the amount of boilerplate code you need to write. Extensibility: The framework is highly extensible, allowing you to customize and extend its functionality to fit your specific needs. Integration with Phoenix: Ash integrates seamlessly with Phoenix, making it easy to build web applications with a powerful backend. Installing Ash Framework To get started with Ash in a new or existing Elixir project, you'll need to add the necessary dependencies to your mix.exs file: defp deps do [ {:ash_phoenix, "~> 2.0"}, {:ash, "~> 3.0"}, # ... other dependencies ] end Then, run mix deps.get to install the dependencies. For more detailed installation instructions and configuration options, check out the Ash Installation Guide. Key Features of Ash Framework Let's explore a few key features of Ash that make it powerful for building applications: 1. Ash Resources In Ash, you define your application's data model using resources. Here's an example of a simple Post resource: defmodule AshBlog.Posts.Post do use Ash.Resource, data_layer: AshPostgres.DataLayer, domain: AshBlog.Posts postgres do table "posts" repo AshBlog.Repo end actions do defaults [:read, :destroy, create: :*, update: :*] end attributes do uuid_primary_key :id attribute :title, :string, allow_nil?: false, public?: true attribute :body, :string, allow_nil?: false, public?: true create_timestamp :inserted_at update_timestamp :updated_at end end This resource definition includes attributes, actions, and database configuration. Ash takes care of generating the necessary database schema and provides a high-level API for interacting with your data. 2. Built-in Pagination One of the powerful features of Ash is its built-in support for pagination. Ash comes with both offset and keyset pagination out of the box. With just a few lines of code, you can implement pagination in your application. To setup keyset pagination in your resource, just add this, under actions: # A `:read` action that returns a paginated list of posts, # with a default of 10 posts per page read :list do pagination keyset?: true, default_limit: 10 prepare build(sort: :inserted_at) end And here’s how you can use it in your LiveView: defp list_posts(%{assigns: %{load_more_token: nil}} = socket) do case Posts.read(Post, action: :list, page: [limit: 10]) do {:ok, %{results: posts}} -> load_more_token = List.last(posts) && List.last(posts).__metadata__.keyset socket |> assign(:load_more_token, load_more_token) |> stream(:posts, posts, reset: socket.assigns.load_more_token == nil) {:error, error} -> put_flash(socket, :error, "Error loading posts: #{inspect(error)}") end end defp list_posts(%{assigns: %{load_more_token: load_more_token}} = socket) do case Posts.read(Post, action: :list, page: [after: load_more_token, limit: 10]) do {:ok, %{results: posts}} -> load_more_token = List.last(posts) && List.last(posts).__metadata__.keyset socket |> assign(:load_more_token, load_more_token) |> stream(:posts, posts, at: -1, reset: socket.assigns.load_more_token == nil) {:error, error} -> put_flash(socket, :error, "Error loading posts: #{inspect(error)}") end end This implementation allows for efficient loading of posts as the user scrolls, creating an infinite scrolling behaviour. 3. Ash.Notifier Another powerful feature of Ash is the ability to broadcast changes in resources using Ash.Notifier. This is particularly useful when you want to update the UI in real-time when data changes. Here's an example of how to set up a notifier: defmodule AshBlog.Notifiers do use Ash.Notifier def notify(%{action: %{type: :create}, data: post}) do Phoenix.PubSub.broadcast(AshBlog.PubSub, "post_creation", {:post_created, post}) end end This notifier broadcasts a message whenever a new post is created. Then, add the notifier to your resource: defmodule AshBlog.Posts.Post do use Ash.Resource, data_layer: AshPostgres.DataLayer, domain: AshBlog.Posts, notifiers: [AshBlog.Notifiers] # <-- add this # ...rest of your code end You can then subscribe to these notifications in your Phoenix LiveView to update the UI in real-time. 4. Integration with Phoenix LiveView Ash integrates seamlessly with Phoenix LiveView, allowing you to build reactive user interfaces. Here's an example of how to use Ash with LiveView: defmodule AshBlogWeb.PostLive.Index do use AshBlogWeb, :live_view alias AshBlog.Posts alias AshBlog.Posts.Post @impl Phoenix.LiveView def mount(_params, _session, socket) do if connected?(socket), do: Phoenix.PubSub.subscribe(AshBlog.PubSub, "post_creation") form = Post |> AshPhoenix.Form.for_create(:create) |> to_form() {:ok, socket |> assign(:page_title, "AshBlog Posts") |> assign(:load_more_token, nil) |> assign(:form, form) |> stream(:posts, [])} end # ... other LiveView callbacks and event handlers end This LiveView lists posts, handles pagination, and updates in real-time when new posts are created. To see a complete example of an Ash-powered blog application, you can check out this sample AshBlog project on GitHub Conclusion Ash framework provides a powerful and flexible way to build Elixir applications. Its declarative approach, resource-based architecture, built-in features like pagination, and integration with Phoenix make it an excellent choice for building complex, data-driven applications.To learn more about Ash and dive deeper into its features, check out the following resources: Ash Documentation AshPhoenix Documentation Phoenix LiveView Documentation Phoenix Framework Guides Happy coding with Ash framework!
Amos Kibet
Post Image

Exciting updates to phx.tools

In the ever-evolving landscape of software development, it's essential to keep our tools lean, efficient, and up-to-date. We're excited to share the latest updates to phx.tools, the complete development environment for Elixir and Phoenix. If you’ve been following our journey since the initial release (as documented in our previous blog post), you’ll appreciate the enhancements we've made to streamline and modernize the toolset. Removal of Unnecessary Software One of the primary goals of this update was to eliminate any bloatware that didn’t contribute directly to the development workflow. We took a closer look at the included software packages and some of the removed packages are: Chrome and Chromedriver: While these tools are useful in some contexts, they aren't always needed for the Elixir and Phoenix development tasks. By removing them, we've reduced the overall footprint of phx.tools, making it more efficient and less resource-intensive. Docker: Docker is a powerful tool, but it’s not a necessity for all developers. Recognizing that not every project requires containerization, we’ve removed Docker to simplify the environment. Developers who need it can still easily install it separately. Node.js: Node.js is a powerful tool, but it’s not a necessity for all Phoenix projects. Recognizing that not every project requires Node.js, we’ve removed it to simplify the environment. Developers who need it can still easily install it separately. These removals not only slim down the installation but also reduce potential security vulnerabilities and maintenance overhead. Updated Software Versions In addition, all the remaining software has been updated to their latest versions. This ensures you have access to the most recent features and improvements, providing a more robust and up-to-date development setup. mise: A Superior Replacement for asdf In this update, we've also replaced asdf with mise as our tool for managing language and package versions. Mise, available at mise.jdx.dev, offers a more streamlined and efficient experience compared to asdf. Performance: Mise is optimized for speed, significantly reducing the time it takes to switch between versions of Elixir, Erlang, or other tools. It also installs multiple tools in parallel. This performance boost helps you maintain your flow without the delays often encountered with asdf. Simplicity: Mise has a more intuitive setup and fewer dependencies, making it easier to configure and use. Unlike asdf, which often requires additional tools like direnv to manage environment variables, mise natively reads your .env file, eliminating the need for external software and simplifying your workflow. Erlang Build Support: When building Erlang, mise automatically takes into account your ~/.kerlrc configuration file, ensuring that your custom settings are applied seamlessly. By adopting mise, we've made phx.tools faster and more user-friendly, ensuring that you have the best possible tools at your disposal. Shell Flexibility Perhaps the most user-friendly update is the change in how we handle shell environments. Previously, we "forced" users to adopt the Zsh shell. While Zsh offers many powerful features, we recognized that forcing a specific shell setup could disrupt developers who were accustomed to their existing environments. phx.tools now automatically detects your current shell configuration and uses it, whether you’re working with Bash or Zsh. This change ensures a smoother, more personalized experience, allowing you to work in the environment you’re most comfortable with. Conclusion The latest update to phx.tools represents our commitment to creating a streamlined, up-to-date, and user-friendly development environment. By removing unnecessary software, updating the remaining tools, and introducing shell flexibility, we’ve made phx.tools more efficient and adaptable to your needs. We’re excited to see how these changes enhance your development experience. As always, we welcome your feedback and look forward to continuing to evolve phx.tools to meet the needs of the Elixir community. Stay tuned for more updates, and happy coding!
Amos Kibet
Post Image

Optimum infrastructure generator

In the Elixir DevOps blog post series we wrote about our development workflows and the infrastructure facilitating them. Those are the tools we reach for on most of the projects. Fly.io is our platform of choice, but even when we’re not the ones making that decision, we at least set up the continuous integration the way we described in the Optimum Elixir CI with GitHub Actions. There are many moving pieces involved in the infrastructure setup, which can incur a great cost in terms of developer hours, even if following along our blog post series. As a small business owner, development team lead, or anyone involved in decision-making, you’ll have a tough time justifying money spent on developers reinventing the wheel which is a CI/CD pipeline and other aspects of infrastructure setup versus taking an off-the-shelf solution. We didn’t want to do this manually on all our projects, so we created a generator that simplifies the process greatly. And now we offer that to everyone else, too. We target startups and small businesses that don’t yet require a huge infrastructure (AWS, Google Cloud, Terraform, Kubernetes, custom setup on bare metal, you name it). Even if you’re already on Kubernetes, maybe you should reconsider whether it’s appropriate for your needs and your scale.   Anyway, the generator serves as a glue between different tools we covered in the blog post series: Optimum CI with a revolutionary yet simple caching strategy automatic deployment to the staging server on Fly.io on merge automatic creation of preview apps on Fly.io on PR creation and updates config for the production server on Fly.io   Plus: AppSignal configuration health check mise setup   Whether you’re working on an existing app, or a completely new one, we got you. Both plain Elixir and Phoenix apps are supported with different feature sets.   If you’re working on an Elixir app without Phoenix you get: mise config (.tool-versions file, reading env variables from .env file) local code checks (compilation warnings, Credo, Dialyzer, dependencies audit, Sobelow, Prettier formatting, Elixir formatting, tests, and coverage) CI on GitHub Actions docs release setup   If you’re working on a Phoenix app, on top of that, you get: health checks AppSignal configuration Dockerfile for environments on Fly.io preview, staging, and production environments config for Fly.io CI and CD on GitHub Actions setup for preview apps and staging deployment   We offer all of this at a predictable, streamlined pricing. The regular price is $499, but for a limited time only, you can get it for $299 $399. Buy it once and run it as many times as you want on any project that you want. We support both plain Elixir and Phoenix apps. Running the generator in Elixir apps sets up mise and CI (locally and on GitHub), while for Phoenix apps it additionally set up CD. Visit hex.codecodeship.com/package/optimum_gen_infra to get started.
Almir Sarajčić
Post Image

Client vs Server side interactions in Phoenix LiveView

The effectiveness of server-side frameworks like Phoenix LiveView for creating fully interactive web applications has sparked considerable debate, as seen in discussions such as these: https://x.com/t3dotgg/status/1796850200528732192 https://x.com/josevalim/status/1798008439895195960 While Phoenix LiveView can achieve significant functionality independently, the strategic decision of when to initiate server round trips becomes crucial for crafting truly interactive web experiences. This post explores the dynamics of client-side versus server-side interactions within Phoenix LiveView. Client vs Server To ensure a smooth user experience, it's crucial to determine which interactions require minimal latency. For instance, actions like dragging and dropping elements across a screen or dynamically creating UI components should be executed without delay; otherwise, your application may feel sluggish and unresponsive. Thus, the decision between client-side and server-side processing hinges on understanding when each approach is most appropriate. Showing and Hiding Content: For interactions such as displaying modals or toggling visibility based on user actions (e.g. clicking a button), handling these tasks on the client side is generally preferable. This approach is suitable unless: The content must be dynamically loaded to optimize network or application load. The interaction requires state changes that must be synchronized with the backend. Example: On a settings page, showing a modal when a user clicks "Change Email Address" can be managed client-side. Showing form in a modal from client-side However, triggering a confirmation message after sending an email with a verification link typically involves a backend state change, making it appropriate for server-side handling. Showing a success message from server-side Zero Latency Demands: Interactions that demand instant responsiveness, such as drag-and-drop interfaces, should primarily be managed client-side to ensure a seamless user experience. Server-Side Necessity: Any interaction that inherently involves the server should be handled server-side. Examples include: Uploading files. Saving data to a database. Broadcasting messages to other clients with Phoenix PubSub. By discerning when to delegate tasks to the client versus the server, applications can optimize performance and responsiveness, delivering an intuitive user experience across various functionalities. How to build a rich client experience in Liveview? LiveView provides developers with convenient ways to incorporate JavaScript code when building interactive applications. Here are some of the available options: LiveView.JS The Phoenix.LiveView.JS module enables developers to seamlessly integrate JavaScript functionality into Elixir code, offering commands for executing essential client-side operations. These commands support common tasks such as toggling CSS classes, dispatching DOM events, etc. While these operations can be accomplished via client-side hooks, JS commands are DOM-patch aware, so operations applied by the JS APIs will stick to elements across patches from the server. https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.JS.html In addition to purely client-side utilities, the JS commands include a rich push API, for extending the default phx-binding pushes with options to customize targets, loading states, and additional payload values. Below is an example demonstrating how to utilize these commands to dynamically apply styles while showing and hiding a modal. <a phx-click={show_settings_modal("change-email-modal")} > Change email address </a> def show_settings_modal(modal) do %JS{} |> JS.add_class("blur-md pointer-events-none", to: ".settings-container") |> JS.show(to: "##{modal}") end JS Hooks A Javascript object provided by phx-hook implementing methods like: mounted(), updated(), beforeUpdate(), destroyed(), disconnected(), reconnected(). For example, one can implement a reorderable drag-and-drop list using Hooks. import Sortablelet Hooks = {} Hooks.Sortable = { mounted(){ let group = this.el.dataset.group let isDragging = false this.el.addEventListener("focusout", e => isDragging && e.stopImmediatePropagation()) let sorter = new Sortable(this.el, { group: group ? {name: group, pull: true, put: true} : undefined, animation: 150, dragClass: "drag-item", ghostClass: "drag-ghost", onStart: e => isDragging = true, // prevent phx-blur from firing while dragging onEnd: e => { isDragging = false let params = {old: e.oldIndex, new: e.newIndex, to: e.to.dataset, ...e.item.dataset} this.pushEventTo(this.el, this.el.dataset["drop"] || "reposition", params) } }) } } let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks}) def render(assigns) do ~H""" <div id="drag-and-drop" phx-hook="Sortable"> ... </div> """ end Learn more about hooks: Client hooks via phx-hook Why is LiveView not a zero-JS framework but a zero-boring-JS framework? Building a simple countdown timer app with LiveView Alpine.js Alpine.js is well-suited for developing LiveView-like applications. While many features of Alpine.js can now be achieved using LiveView.JS or Hooks, it remains prevalent in numerous codebases, especially those originally built on the popular PETAL stack during the earlier days of Phoenix LiveView. Alpine.js is still useful for handling events not covered with LiveView.JS. Here's an example demonstrating Alpine.js usage to toggle and transition components: <div x-data="{ isOpen: false }"> <button @click="isOpen = !isOpen" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"> Toggle Component </button> <div x-show.transition="isOpen" class="bg-gray-200 p-4 mt-2"> <!-- Your content here --> This content will toggle with a nice transition effect. </div> </div> One can combine JavaScript commands with Alpine.js effectively. For instance, you can dispatch DOM events when a button is clicked and handle them in Alpine.js: <button phx-click={JS.dispatch("set-slide", detail: %{slide: "chat"})}> Open Chat </button> # Elsewhere in the codebase <div x-data="{ slide: null }" @set-slide-over.window="slide = (slide == $event.detail.slide) ? null : $event.detail.slide" > ... </div> In this example, clicking the button dispatches a custom event (set-slide) with specific data. Alpine.js then listens for and handles this event, demonstrating seamless integration with JavaScript commands in a mixed codebase environment. Check out Alpine.js here. Built-in defaults to enrich client-side interactions LiveView includes client-side features that allow developers to provide users with instant feedback while waiting for actions that may have latency. Some of these features include: phx-disable-with: This feature allows buttons to switch text while a form is being submitted. <button type="submit" phx-disable-with="Updating...">Update</button> The button's innerText will change from "Update" to "Updating..." and restore it to "Update" on acknowledgment. LiveView's CSS classes LiveView includes built-in CSS classes that facilitate providing feedback. For instance, you can dynamically swap form content while a form is being submitted using LiveView's CSS loading state classes. .while-submitting { display: none; } .inputs { display: block; } .phx-submit-loading .while-submitting { display: block; } .phx-submit-loading .inputs { display: none; } <form phx-change="update"> <div class="while-submitting">Please wait while we save our content...</div> <div class="inputs"> <input type="text" name="text" value={@text}> </div> </form> You can learn more about this here. Global events dispatched for page navigation: LiveView emits several events to the browsers and allows developers to submit their events too. For example, phx:page-loading-start and phx:page-loading-stop are dispatched, providing developers with the ability to give users feedback during main page transitions. These events can be utilized to display or conceal an animation bar that spans the page as shown below. // Show progress bar on live navigation and form submits topbar.config({...}) window.addEventListener("phx:page-loading-start", info => topbar.show()) window.addEventListener("phx:page-loading-stop", info => topbar.hide()) Other resources Optimizing user experience in LiveView phx- HTML attributes cheatsheet JavaScript interoperability
Nyakio Muriuki
Post Image

Zero downtime deployments with Fly.io

If you were wondering why you saw the topbar loading for ~5 seconds every time you deployed to Fly.io, you’re at the right place. We need to talk about deployment strategies. Typically, there are several, but Fly.io supports these: immediate rolling bluegreen canary   The complexity and cost go from low to high as we go down the list. The default option is rolling. That means, your machines will be replaced by new ones one by one. In case you only have one machine, it will be destroyed before there’s a new one that can handle requests. That’s why you’re waiting to be reconnected whenever you deploy. You can read more about these deployment strategies at https://fly.io/docs/apps/deploy/#deployment-strategy.   We’re using the blue-green deployment strategy as it strikes a balance between the benefits, cost, and ease of setup.   If you’re using volumes, I have to disappoint you as the blue-green strategy doesn’t work with them yet, but Fly.io plans to support that in the future.   Setup You need to configure at least one health check to use the bluegreen strategy. I won’t go into details. You can find more at https://fly.io/docs/reference/configuration/#http_service-checks.   Here’s a configuration we use: [[http_service.checks]] grace_period = "10s" interval = "30s" method = "GET" path = "/health" timeout = "5s"   Then, add strategy = “bluegreen” under [deploy] in your fly.toml file: [deploy] strategy = "bluegreen" and run fly deploy.   That’s it! You probably expected the setup to be more complex than this. So did I!   Conclusion While Fly.io is moving you from a blue to a green machine, your websocket connection will be dropped, but it will quickly reestablish. You shouldn’t even notice it unless you have your browser console open or you’re navigating through pages during the deployment.   One thing you should keep in mind, though, is that your client-side state (form data) might be lost if you don’t address that explicitly.   Another thing to think about is the way you run Ecto migrations. In case you’re dropping tables or columns, you might want to do that in multiple stages. For example, you might introduce changes in the code so you stop depending on specific columns or tables and deploy that change. After that, you can have subsequent deployment for the structural changes of the database. That way, both blue and green machines will have the same expectations regarding the database structure.   The future will bring us more options for deployment. Recently, Chris McCord teased us with hot deploys.   https://x.com/chris_mccord/status/1785678249424461897   Can’t wait for this!   This was a post from our Elixir DevOps series.
Almir Sarajčić
Post Image

Feature preview (PR review) apps on Fly.io

In this blog post, I explain how we approach manual testing of new features at Optimum.   Collaborating on new features with non-developers often requires sharing our progress with them. We can do quick demos in our dev environment, but if we want to let them play around on their own, we need to provide them with an environment facilitating that. Setting up a dev machine is easy thanks to phx.tools: Complete Development Environment for Elixir and Phoenix, but pulling updates in our projects still requires basic git knowledge.   We could solve this by deploying in-progress stuff to the staging server, but that becomes messy in larger teams, so we stay away from that. Instead, we replicate the production environment for each feature we are working on and we only deploy the main branch with finished features to the staging. With an environment created specifically for the feature we are working on, we can be sure nothing will surprise us after shipping it. Automated tests help with that too, but we still like doing manual checks just before deploying to production.   Heroku review apps Back when I was working on Ruby on Rails apps and websites, as many, I chose Heroku as my PaaS (platform-as-a-service). It had (and still has) a great feature called Review apps. App deployment pipeline on Heroku   It enables you to create new Heroku environments in your pipeline for the PRs in your GitHub repo, either manually through their UI or automatically using a configuration file. You can configure the dynos, environment variables, addons… any prerequisite for running your application. This was a great experience when I worked with Ruby, but when I moved to Elixir Heroku didn’t fit me anymore, so I moved to Fly.io.   Fly.io PR review apps Fly.io introduced something similar using GitHub Actions: https://github.com/superfly/fly-pr-review-apps. It’s not as powerful and is not as user-friendly, but it’s a good starting point for building your workflows. Here’s the official guide: https://fly.io/docs/blueprints/review-apps-guide/.   The current version forces you to share databases and volumes between different PR review apps. We didn’t want that, so last year my colleague Amos introduced a fork that solves this, accompanied by the blog post How to Automate Creating and Destroying Pull Request Review Phoenix Applications on Fly.io. We’ve also added some minor changes there. Some of them were implemented upstream since then, yet the setup for the database and volume is still missing. Here’s the diff: https://github.com/superfly/fly-pr-review-apps/compare/6f79ec3a7d017082ed11e7c464dae298ca75b21b...optimumBA:fly-preview-apps:b03f97a38e6a6189d683fad73b0249c321f3ef4a.   Examples We use preview apps for our phx.tools website. Although it doesn’t use DB and volumes, it’s still a good example of setting preview apps up on Fly.io: https://github.com/optimumBA/phx.tools/blob/main/.github/github_workflows.ex.   Here’s the code responsible for preview apps: @app_name "phx-tools" @environment_name "pr-${{ github.event.number }}" @preview_app_name "#{@app_name}-#{@environment_name}" @preview_app_host "#{@preview_app_name}.fly.dev" @repo_name "phx_tools" defp pr_workflow do [ [ name: "PR", on: [ pull_request: [ branches: ["main"], types: ["opened", "reopened", "synchronize"] ] ], jobs: elixir_ci_jobs() ++ [ deploy_preview_app: deploy_preview_app_job() ] ] ] end defp pr_closure_workflow do [ [ name: "PR closure", on: [ pull_request: [ branches: ["main"], types: ["closed"] ] ], jobs: [ delete_preview_app: delete_preview_app_job() ] ] ] end defp delete_preview_app_job do [ name: "Delete preview app", "runs-on": "ubuntu-latest", concurrency: [group: "pr-${{ github.event.number }}"], steps: [ checkout_step(), [ name: "Delete preview app", uses: "optimumBA/fly-preview-apps@main", env: [ FLY_API_TOKEN: "${{ secrets.FLY_API_TOKEN }}", REPO_NAME: @repo_name ], with: [ name: @preview_app_name ] ], [ name: "Generate token", uses: "navikt/github-app-token-generator@v1.1.1", id: "generate_token", with: [ "app-id": "${{ secrets.GH_APP_ID }}", "private-key": "${{ secrets.GH_APP_PRIVATE_KEY }}" ] ], [ name: "Delete GitHub environment", uses: "strumwolf/delete-deployment-environment@v2.2.3", with: [ token: "${{ steps.generate_token.outputs.token }}", environment: @environment_name, ref: "${{ github.head_ref }}" ] ] ] ] end defp deploy_job(env, opts) do [ name: "Deploy #{env} app", needs: [ :compile, :credo, :deps_audit, :dialyzer, :format, :hex_audit, :prettier, :sobelow, :test, :test_linux_script_job, :test_macos_script_job, :unused_deps ], "runs-on": "ubuntu-latest" ] ++ opts end defp deploy_preview_app_job do deploy_job("preview", permissions: "write-all", concurrency: [group: @environment_name], environment: preview_app_environment(), steps: [ checkout_step(), delete_previous_deployments_step(), [ name: "Deploy preview app", uses: "optimumBA/fly-preview-apps@main", env: fly_env(), with: [ name: @preview_app_name, secrets: "APPSIGNAL_APP_ENV=preview APPSIGNAL_PUSH_API_KEY=${{ secrets.APPSIGNAL_PUSH_API_KEY }} PHX_HOST=${{ env.PHX_HOST }} SECRET_KEY_BASE=${{ secrets.SECRET_KEY_BASE }}" ] ] ] ) end defp delete_previous_deployments_step do [ name: "Delete previous deployments", uses: "strumwolf/delete-deployment-environment@v2.2.3", with: [ token: "${{ secrets.GITHUB_TOKEN }}", environment: @environment_name, ref: "${{ github.head_ref }}", onlyRemoveDeployments: true ] ] end defp fly_env do [ FLY_API_TOKEN: "${{ secrets.FLY_API_TOKEN }}", FLY_ORG: "optimum-bh", FLY_REGION: "fra", PHX_HOST: "#{@preview_app_name}.fly.dev", REPO_NAME: @repo_name ] end defp preview_app_environment do [ name: @environment_name, url: "https://#{@preview_app_host}" ] end   If you’re wondering why you’re seeing Elixir while working with GitHub Actions you should read our blog post on the subject: Maintaining GitHub Actions workflows.   Let’s explain what we’re doing above. We are running the pr_workflow when PR is (re)opened or when any new changes are pushed to it. It runs our code checks and tests, and, if everything passes, runs the deploy_preview_app_job.   GitHub Actions workflow for PRs   The deploy_preview_app_job uses action for deploying preview apps to Fly.io which checks if the server is already set up. If it isn’t, it creates the server, sets environment variables, etc. Then it deploys to it.   Preview app creation job that includes a DB and/or a volume doesn’t differ from the one above at all. That’s because our action optimumBA/fly-preview-apps internally checks whether an app contains Ecto migrations and if it does, it creates a DB if it doesn’t exist yet. The same goes for the volume: it checks whether the fly.toml configuration contains any mounts and if it does, it creates a volume, then attaches it to the app.   GitHub workflow for the website you’re on   Preview app for one of the PRs   We set environment to let GitHub show the preview app in the list of environments. It will show the status of the latest deployment in the PR. We don’t want too much noise in our PR from the deployment messages, so whenever we deploy a new version, we remove previous messages in the delete_previous_deployments_step.   List of deployments on GitHub   Deployment status message in the PR   Setting concurrency makes sure that two deployment jobs can’t run simultaneously for the same value passed to it. That prevents hypothetical race condition with multiple pushes, where for some reason deployment job for the latest commit could finish more quickly than the one for the previous commit, which would leave us with an older version of the app running.   Don’t forget to set GitHub secrets like FLY_API_TOKEN. You might want to do that on the organization level so you don’t have to do that for every repo. The token we’ve set in our GitHub organization is for a Fly.io user we’ve created specifically for deployments to staging and preview apps. We have a separate Fly.io organization it is a part of, so even if the token gets leaked, our production apps are safe as it doesn’t have access to them.   When we’re done working on a feature, we want to clean up our environment. It might seem strange that we use the same action to delete our app, but the action handles it by checking the event that triggered the workflow and acts accordingly. It destroys any associated volume and/or database, then the server. The next two steps of the delete_preview_app_job delete a GitHub environment. For some reason known to GitHub, the process is more complicated than it should be, but Amos explains it well in his blog post.   Getting back to the part about databases. Recently, the upstream version of the action was updated with an option to attach an existing PostgreSQL instance from Fly.io, but that still doesn’t solve potential issues with migrations. Let’s say you remove a table in one PR, while another PR depends on the same table. It will be deleted while deploying the first PR which will in turn cause errors for the second PR’s review app. Our solution avoids that by creating a completely isolated environment for each PR.   Additionally, Fly.io recently introduced (or we’ve just discovered) the ability to stop servers after some time of inactivity. That proved useful for us in lowering the cost when having many PRs open. In your fly.toml you probably want to set [http_service] auto_start_machines = true auto_stop_machines = true min_machines_running = 0   so your machines stop if you don’t access them for some period. We haven’t found a way to stop DBs for inactive apps yet. We weren’t eager to do so, though, because we’ve always had the smallest instances for the preview apps DBs. Only our apps sometimes have larger instances which incur greater costs, so we see a benefit in stopping them when we don’t use them.   More customization Some applications might require setting up additional resources. In the StoryDeck app, one of the services we use is Mux.   When a user uploads a video, we upload it to Mux, which sends us events to our webhooks. Whenever we create a new preview app, we need to let Mux know the URL of our new webhook. In theory, this could be solved by a simple proxy. In reality, it’s more complicated than that. We don’t want all our preview apps to receive an event when a video is uploaded from any of them. To know which preview app to proxy an event to, the proxy app would need to store associations between specific videos and preview apps they were uploaded from, but we don’t want to store that kind of data in the proxy app. Mux enables having many different environments in one account, which is perfect for us as each environment is a container for videos uploaded from one preview app. What is not perfect is the fact that currently there’s no API for managing Mux environments, so we have to do it through the Mux dashboard. We’ve built the proxy app using Phoenix. It has a simple API on which we receive requests sent from GitHub Actions using curl. When a new preview app is created, a request is received, then the app goes through the Mux dashboard using Wallaby, creates a new Mux environment, sets up the webhook URL, gets Mux secrets, and returns it so that the GitHub Actions workflow can set them in our new Fly.io environment. When deleting the preview app, our workflow sends a request to our proxy app which then deletes videos from Mux and deletes the Mux environment.   Creating Mux environment and saving credentials in GitHub Actions cache   That is just one example of what it might take to enable preview apps in your organization. It could seem like unnecessary work, but think of it as an investment into higher productivity and quality of work down the line.   This was a post from our Elixir DevOps series.
Almir Sarajčić

Portfolio

  • Phx.tools

    Powerful shell script designed for Linux and macOS that simplifies the process of setting up a development environment for Phoenix applications using the Elixir programming language. It configures the environment in just a few easy steps, allowing users to start the database server, create a new Phoenix application, and launch the server seamlessly. The script is particularly useful for new developers who may find the setup process challenging. With Phoenix Tools, the Elixir ecosystem becomes more approachable and accessible, allowing developers to unlock the full potential of the Phoenix and Elixir stack.

    Phx.tools
  • Prati.ba

    Bosnian news aggregator website that collects and curates news articles from various sources, including local news outlets and international media. The website provides news on a variety of topics, including politics, sports, business, culture, and entertainment, among others.

    Prati.ba
  • StoryDeck

    StoryDeck is a cloud-based video production tool that offers a range of features for content creators. It allows users to store and archive all their content in one location, track tasks and collaborate easily with team members, and use a multi-use text editor to manage multiple contributors. The platform also offers a timecode video review feature, allowing users to provide precise feedback on video files and a publishing tool with SEO optimization capabilities for traffic-driving content.

    StoryDeck