AppFlowy-IO / AppFlowy-Cloud

AppFlowy is an open-source alternative to Notion. You are in charge of your data and customizations. Built with Flutter and Rust.
GNU Affero General Public License v3.0
1.02k stars 215 forks source link

[Bug] Self-hosted : Publish to web : not supported #680

Open almereyda opened 3 months ago

almereyda commented 3 months ago

Describe the bug

Since v0.6.3 AppFlowy supports to publish to the web.

This is currently not available in self-hosted AppFlowy Cloud, as per @LucasXu0's comment in https://github.com/flathub/io.appflowy.AppFlowy/pull/112#issuecomment-2216130802

Published documents will show to be generated on appflowy.com instead of the custom deployment's domain.

grafik

Opening the offered route with the local deployment's FQDN instead of appflowy.com does not yield any meaningful response.

Expected behavior

Publish to web is available for AppFlowy self-hosters.

Additional Context

We have four highly related tracking issues for two features in two projects now:

Feature AppFlowy AppFlowy Cloud
Share to web https://github.com/AppFlowy-IO/AppFlowy/issues/5920 \<this one>
Collaborate on web https://github.com/AppFlowy-IO/AppFlowy/issues/6539 https://github.com/AppFlowy-IO/AppFlowy-Cloud/issues/873
therynamo commented 2 months ago

I'd love to understand the tracking of this issue, and if it's been taken up yet. I'm not sure exactly what would need to be done, but this is something that would be very useful.

I'd like to be able to write documents, and then "publish" them, but only on my self-hosted instance. That way when individuals arrive in my lab, they can visit a URL to view my document(s) instead of having to log into AppFlowy. (Given there are currently limitations around shared workspaces when self-hosting, it'd likely be the "admin" or "single user" they'd have to login with as well, which I wouldn't love.)

Thanks again for all the hard work on this project, you all are really taking on a lot, and I'm excited to see it keep growing 👍

almereyda commented 3 weeks ago

There is now a proof of concept for self-publishing to the web with AppFlowy. @Hazegard has shown a presumably working example in https://github.com/AppFlowy-IO/AppFlowy/issues/5920#issuecomment-2380093143, which asks for replication and verification here.

Related:

almereyda commented 3 weeks ago

In replicating the example with the referenced deployment, I was able to validate the path and document it further.

This setup uses the build container from the example and extends it with an adapted AppFlowy-IO/AppFlowy frontend/appflowy_web_app/deploy/Dockerfile.

As in the example, the container is published to a separate domain. This does not replace the existing AppFlowy interface, nor does it make the published URL configurable. One needs to copy a published address and swap the domain manually for the time being.

  appflowy_web_app:
    build:
      context: ./context/web
      args:
        AF_DEFAULT_SITE: "${AF_DEFAULT_SITE}"
        AF_VERSION: "${AF_VERSION}"
        APPFLOWY_CLOUD_DOMAIN: "${APPFLOWY_CLOUD_DOMAIN}"
    environment:
      - AF_BASE_URL
    networks: ["web"]
    expose: ["80"]
    labels:
      traefik.enable: true
      traefik.http.routers.org-example-content-web.entrypoints: web
      traefik.http.routers.org-example-content-web.rule: Host(`${WEB_APP_FQDN}`)
      traefik.http.routers.org-example-content-web.middlewares: http-to-https
      traefik.http.middlewares.http-to-https.redirectscheme.scheme: https
      traefik.http.middlewares.http-to-https.redirectscheme.permanent: true
      traefik.http.routers.org-example-content-webs.entrypoints: webs
      traefik.http.routers.org-example-content-webs.rule: Host(`${WEB_APP_FQDN}`)
      traefik.http.routers.org-example-content-webs.tls: true
      traefik.http.routers.org-example-content-webs.tls.certresolver: le

.env, AF_ prefixed variables point at an existing AppFlowy Cloud instance.

APPFLOWY_CLOUD_DOMAIN=appflowy.example.org
WEB_APP_FQDN=content.example.org
AF_VERSION=0.7.0
AF_BASE_URL=https://${APPFLOWY_CLOUD_DOMAIN}
AF_DEFAULT_SITE=https://example.org
#AF_GOTRUE_URL=${AF_BASE_URL}/gotrue
#AF_WS_URL=${AF_BASE_URL}/ws/v1

Taken from #622, this adds a variable containing an allow list for known CORS origins, an expression that matches our content.example.org frontend build and changes the if condition to test non-empty matches.

11a12,16
>     map "$http_origin" $allow_origin {
>         default '';
>         "~^https://content\.allmende\.io$" "$http_origin";
>     }
> 
63c68
<                 add_header 'Access-Control-Allow-Origin' $http_origin;
---
>                 add_header 'Access-Control-Allow-Origin' $allow_origin;
99,100c104,105
<             if ($http_origin ~* (http://127.0.0.1:8000)) {
<                 add_header 'Access-Control-Allow-Origin' $http_origin always;
---
>             if ($http_origin != "") {
>                 add_header 'Access-Control-Allow-Origin' $allow_origin always;

This step would probably have to be repeated for the options endpoint, if one wanted to replace the whole AppFlowy web interface at the origin domain with this build.

The deploy container does not build an exactly exemplary production container setup. It uses the discouraged fat-container pattern to run an init process inside the container, which schedules other jobs.

In the original container from the deploy/ directory, this Supervisor instance only launches a single command, which then in return indirectly and directly spawns the processes. Which seems one layer of abstraction too much for no real gain.

When building the container, we patch the nginx.conf file to set daemon off;, which allows it to run in foreground and thus inside Supervisor. And then we can just run the Bun server directly, instead of using deploy/start.sh.

[supervisord]
nodaemon=true

[program:bun]
command=bun run server.ts
autostart=true
autorestart=true
stderr_logfile=/var/log/bun.err.log
stdout_logfile=/var/log/bun.out.log

[program:nginx]
command=/usr/sbin/nginx
autostart=true
autorestart=unexpected
exitcodes=0
stdout_logfile=/var/log/nginx.out.log
stderr_logfile=/var/log/nginx.err.log

./context/web/Dockerfile

It seems the build container modifies the files that come up when searching the repository for test.appflowy.cloud. Some of those values are defaults that can be changed by environmental variables (see AF_GOTRUE_URL and AF_WS_URL). Others could be parametrised and be made configurable for ease of build.

Ideally, the replace value can be provided at runtime and the client dynamically reconfigures itself to work against self-hosted AppFlowy instances.

The replace value could then also be used to replace the appflowy.com domain in the Publish modal dynamically.

FROM alpine:latest AS build

ARG AF_VERSION=main
ARG APPFLOWY_CLOUD_DOMAIN=appflowy.cloud

WORKDIR /tmp
RUN apk add git nodejs npm && \
    npm install -g pnpm@8.5.0
RUN git clone --depth 1 --branch "${AF_VERSION}" https://github.com/AppFlowy-IO/AppFlowy

WORKDIR /tmp/AppFlowy/frontend/appflowy_web_app

RUN sed -i "s/test.appflowy.cloud/${APPFLOWY_CLOUD_DOMAIN}/g" \
      /tmp/AppFlowy/frontend/appflowy_tauri/src-tauri/env.development \
      /tmp/AppFlowy/frontend/appflowy_web_app/src/components/main/app.hooks.ts \
      /tmp/AppFlowy/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts \
      /tmp/AppFlowy/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart \
      /tmp/AppFlowy/frontend/appflowy_tauri/src/appflowy_app/components/_shared/devtool/ManualSignInDialog.tsx \
      /tmp/AppFlowy/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart \
      /tmp/AppFlowy/frontend/appflowy_flutter/integration_test/desktop/settings/sign_in_page_settings_test.dart \
      /tmp/AppFlowy/frontend/appflowy_flutter/integration_test/desktop/settings/sign_in_page_settings_test.dart 
RUN pnpm install && pnpm run build

FROM oven/bun:latest AS app

ARG AF_DEFAULT_SITE=https://appflowy.io

RUN apt-get update && \
      apt-get install -y nginx supervisor && \
      apt-get clean && \
      rm -rf /var/lib/apt/lists/*
RUN addgroup --system nginx && \
      adduser --system --no-create-home --disabled-login --ingroup nginx nginx

WORKDIR /app
RUN bun install cheerio pino pino-pretty

COPY --from=build /tmp/AppFlowy/frontend/appflowy_web_app/dist /usr/share/nginx/html/
COPY supervisord.conf /app/supervisord.conf
ADD https://github.com/AppFlowy-IO/AppFlowy/raw/refs/heads/main/frontend/appflowy_web_app/deploy/nginx.conf /etc/nginx/nginx.conf
ADD https://github.com/AppFlowy-IO/AppFlowy/raw/refs/heads/main/frontend/appflowy_web_app/deploy/server.ts /app/server.ts

RUN chmod +x /app/supervisord.conf

RUN sed "s|https://appflowy.io|${AF_DEFAULT_SITE}|g" -i /app/server.ts
RUN sed "4i daemon off;" -i /etc/nginx/nginx.conf
RUN ln -s /usr/share/nginx/html dist

EXPOSE 80
CMD ["supervisord", "-c", "/app/supervisord.conf"]

The Dockerfile adds build arguments, reorders some commands and uses a two stage build process. The first part is used as builder and the second copies the dist from it, integrating the build artefact with its expected deployment environment aside server.ts. For now, we don't need to bother too much what it does. In short it helps us to reproduce the production environment as closely as possible.

Funnily, we also add a quirk in form of a symlink, because this Dockerfile wouldn't work and always yielded Internal Server Error without it, because index.html could not be found. The origin hasn't been tested, but there may be a regression as well.

Apart from serving the App for every route, which could also be implemented in a static file server, this also injects page metadata into the else static page header, which renders the structured metadata discoverable by search engines and URL preview bots.

Steps to run:

docker compose build appflowy_web_app
docker compose up -d appflowy_web_app

It is then possible to open a published document at not https://appflowy.com/1b48b428-3871-42b7-b0e9-296a5e6be05b/Example-65ff6260-1af6-41b5-bd0f-e9de80df5714 but with the custom domain for the new frontend instead. Here: content.example.org

Which gives us the published document and three AppFlowy icons in all corners but the lower-right. In addition, we are also offered to "Start with this template" and a three-dot menu, where we are suggested to "Sign up or log in". For a static export and esp. for a self-hosted environment, all these are too much self-promotion and nudging towards registration with the platform and potentially distract a recipient from the content that was tried to convey with the link.

When being finished reading, at the bottom there is another Comments area, which when engaged with also tries to have us register. Again.

Screenshot 2024-09-28 at 05-50-09 Sandkasten AppFlowy

The first visible regression is that "Image load failed" is displayed instead of an image, because its file_storage URL returns:

{"code":1017,"message":"Can't find the user uuid from the request: No Authorization header"}

The second not-really regression is, that linked sub-pages of a document are not automatically published. References to them appear as "No access".

grafik

Publishing the documents then has the titles appear.

The third regression is, that renaming and changing the symbol of a domain is not reflected in the published view, when a domain was duplicated. Changing the domain duplicate will then change the display of the domain for the documents, which were published from the source. There might be some little hickups somewhere in this area of the application.

Apart from the fact that it is hard not to click somewhere that displays a registration screen, documents get published.

Unpublishing also gives the expected "No access to this page..." upon reload.

For now, this kind of setup appears to present a suitable workaround for allowing publishing to the web with self-hosted AppFlowy-Cloud.

In an ideal world, AppFlowy has a runtime configuration that allows the Desktop, Mobile and Web apps to adapt their configuration to a self-hosted installation, in so custom builds are not required.

luxio commented 2 weeks ago

@almereyda Thank you for sharing this.

Do I understand correctly that attached images and documents are not displayed on the published page?

almereyda commented 2 weeks ago

Yes, attached files don't make it, yet.