How to authenticate users directly in Shiny without having a separate authentification layer before starting shiny?

Is this a good approach?

Seems very fair to me. Good work!

What happens to reactive elements that have been created, but are not used anymore? Could this affect performance?

The input reactive list will be new when the browser refreshes. The input reactive list is a different reactive list for each individually loaded webpage (a Shiny session). We only need to safeguard against successive logins without a page refresh.

During a session, reactive values persist until they are invalidated. I would instead make the user information an isolated reactiveVal, rather than a value within a reactiveValues. This is so that the reactiveVal object can be passed into the mod_private_app code. (Could also be done by passing in the whole values reactiveValues object and retrieving values$user each time the user is needed.)

The statement renderPrint({paste0("Hello ", .user)}), would be changed to renderPrint({paste0("Hello ", .user())}). If the .user() object is invalidated, then all reactives that depend on it will be invalidated as well (effectively wiping their state). Remember that the user reactive value doesn't necessarily need to be used in the output, as long as it is used in the reactive expression to invalidate the object being produced.

In your observeEvent for logout you would set user to NULL and all outputs that are a reactive child of user() will be invalidated as well.

user <- reactiveVal(NULL)

observe({
  req(user())
  output$ui <- renderUI({
    mod_private_app_ui(paste0("private", user()))
  })

  logout <- callModule(mod_private_app, paste0("private", user()), .user = user)

  # (this code could be put inside mod_private_app definition.)
  observeEvent(logout(), {
    user(NULL)
  })
})