Open-source project
logamee/lin-cms-flask avatar
logamee/lin-cms-flask

Lin-CMS-Flask is a CMS backend that assumes a Vue frontend exists

🎀A simple and practical CMS implememted by Flask

840 stars218 forksPythonNOASSERTION

At a glance

What is it?
Lin-CMS-Flask is the Flask backend of the Lin CMS framework, a JSON API for user management, permissions, logging and validation that expects a separate single page frontend and ships plugins, migrations, Docker Compose deployment and a CI gate. It suits teams who want a working admin backend to extend rather than a CMS to configure.
Who is it for?
Adopt Lin-CMS-Flask when you want a JSON API backend with users, permissions, logging, parameter validation and a plugin mechanism already in place, and you are willing to build the frontend against its conventions. Do not adopt it expecting a server rendered admin site, because the project is explicitly a front end and back end separated CMS and the pages are built by a separate Vue repository.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A backend with no templates in sight

The project is the Flask implementation of the Lin CMS backend, and the first thing the README settles is that the frontend lives elsewhere. The documentation asks you to visit the separate frontend repository if you need one, and the design section explains the reasoning. Lin CMS is a front end and back end separated CMS, and the argument is that server side rendering is fine for public sites where SEO matters, but not for an internal CMS where SEO does not matter, and that the page work sits awkwardly with both sides, since few front end developers now write Jinja2 or Jade templates while back end developers are not the people who should be building the page. The result is that the server only concerns itself with its API while the front end is written in Vue. A live demo is hosted, and the repository carries a QQ group and a WeChat public account for support.

Users, permissions, logs and validation are already there

The framework ships the parts of a CMS that every project ends up writing. The README lists user management, permission management and a logging system as built in, so the developer can concentrate on the actual CMS business. It also presents Lin CMS as a set of development conventions rather than only a feature list, with the argument that you should not have to keep wondering how to validate parameters, how to operate the database, how to handle global exceptions, how the API should be structured or how the front end should be organised. The validation example given is telling. Every API needs to validate the parameters the client sends, there are many ways to do that, and Lin provides one mechanism so that a developer only has to imitate the existing approach. That is a real trade-off being made: consistency and speed at the cost of a house style you did not choose.

Extensions come as plugins

Lin CMS extends through a plugin idea, and the README describes both routes. When you need a new feature you can write the code directly in the Lin directory, or you can encapsulate the feature as a plugin. The example given is an article management feature that you can publish as a plugin so that other developers install it and reuse it, and the README goes as far as saying you can publish your own plugins for other developers to use. The repository layout backs this up, with the application code under `app/` and a `app/plugin/` area in the tree, alongside `app/lin/` for the framework itself and the `starter.py` entry point. One practical consequence appears in the migration workflow, since the README warns that plugin tables such as the poem, oss and cos plugins are only registered when the plugin is enabled, so a plugin being switched on means a new incremental migration has to be generated or the schema drifts.

Getting it running with uv and SQLite

The environment requirements are strict and stated up front. Python 3.11 or later is needed, with 3.10 no longer maintained and no longer supported by the current dependency chain, and uv is the package manager, replacing the older poetry setup, with every command run through `uv run`. The first four commands take you from a fresh clone to a running server:

bash
cp .env.example .development.env
uv sync
uv run flask db init
uv run flask run

The first step is marked as mandatory, because the template is where the SECRET_KEY goes and a missing secret makes the application refuse to start. The `db init` step creates the tables and the seed data, and the README flags the default account, root with the password 123456, as something to change quickly. The development server defaults to SQLite and reads `.development.env`. SQLite is the default database for a reason, since it needs no separate installation, and switching to a server database is a matter of installing the driver and changing the `SQLALCHEMY_DATABASE_URI` value in the environment file, with extras named `mysql` for PyMySQL and `postgres` for psycopg 3.

flask db init is not what it sounds like

The migration story has a deliberate naming trap and the README explains it. The database schema is managed by Flask-Migrate on top of Alembic, and the command habits are kept identical to the older version. `uv run flask db init` creates the tables and seeds the data, including the root account, a Guest group and the permission sync, and it is idempotent, so running it again has no side effects. `uv run flask db init --force` clears and rebuilds, and `uv run flask db fake` adds sample book data for demonstration purposes. Under the hood `flask db init` is `flask db upgrade`, with Alembic recording versions in `alembic_version`. The native Alembic `init`, which would create a migration repository, is intentionally shadowed, because the `migrations/` directory ships with the repository and needs no scaffolding. After changing a model you run `flask db migrate -m` with a message, which uses Alembic's automatic batch mode on SQLite, and then `flask db upgrade` to apply it. An existing database is brought onto the migration system with `flask db stamp head`, which registers the version without running any DDL.

The decorator order that CI exists to catch

One rule in the project is unusual enough that the README gives it its own section, and the test suite enforces it. `@permission_meta` matches permission metadata by function identity, so it must sit directly against the route decorator and above any decorator that wraps the function, such as a rate limiter:

python
@cms_api.route("/xxx", methods=["POST"])
@permission_meta(name="some action", module="some module", mount=False)
@limiter.limit("60 per minute")
@api.validate(...)
def your_view(json: SomeSchema):
    ...

The failure mode is quiet. Reversing the order does not raise an error, but the permission declaration is never synchronised into the permission table, and existing rows can then be treated as stale and cleaned up. The backstop is `tests/test_permission_sync.py` in CI. The rest of the quality setup is visible too. `uv run pytest` runs the full suite against an in-memory SQLite database that is isolated from the development database and can run in any order, coverage is collected with the coverage tool, and pre-commit installs hooks for isort, black, flake8 and mypy. CI runs the same checks on every push and pull request, adds a fresh database migration and a model drift check, and fails if total coverage drops below 70 percent.

Docker Compose, the health probe and the release pipeline

The production path is documented as a short sequence. A secret has to be provided in the same directory as a `.env` file or in the environment, and the Compose file makes it mandatory with a shell parameter expansion, so the container refuses to start without it. Then the stack is built and started, and the health endpoint is checked:

bash
echo "SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex(32))')" > .env
docker compose up -d --build
curl http://127.0.0.1:5000/health

The health check needs no authentication, and the container image deliberately has no curl or wget, so the Compose health check calls the endpoint with the Python standard library instead. The container runs as a non-root user, performs migrations automatically at startup, seeds a fresh empty database with the root account, and keeps SQLite data in the named volume `instance-data` so that rebuilding the container does not lose it. The Dockerfile is built from `python:3.14` and the application starts under gunicorn with gevent. Pushing a `v*` tag triggers a release workflow that reuses every CI quality gate, then builds the image, pushes it to ghcr.io and creates a GitHub Release.

Dependency export, licences and a project that moved recently

Two operational details close the picture. The `requirements.txt` file is exported from `uv.lock` and the README asks that it not be edited by hand, with the regeneration commands given for the default, MySQL and PostgreSQL variants, so upgrading dependencies means running `uv lock --upgrade`, then `uv sync`, then the test suite. The lock file and the export are the mechanism that keeps the Docker image reproducible. On licensing, the README badge shows MIT, while GitHub classifies the repository licence as Other, meaning a custom licence that the platform cannot map to a standard identifier, so the LICENSE file is the text to read if your compliance process needs a named grant. On maintenance, the release history shows a long gap followed by a burst: 0.4.0 and 0.4.3 in February and March 2022, then v0.7.0 on 2026-10-14, with the last push on the same day and the project not archived.

Editorial conclusion

Adopt Lin-CMS-Flask when you want a JSON API backend with users, permissions, logging, parameter validation and a plugin mechanism already in place, and you are willing to build the frontend against its conventions. Do not adopt it expecting a server rendered admin site, because the project is explicitly a front end and back end separated CMS and the pages are built by a separate Vue repository. Verify first that your Python version is inside the supported range of 3.11 to 3.14, that you set a SECRET_KEY before starting, and that the default seeded credentials are changed, since the seed creates a root account with the password 123456.

Frequently asked questions

How do I run Lin-CMS-Flask locally?

Copy `.env.example` to `.development.env`, fill in the SECRET_KEY, then run `uv sync`, `uv run flask db init` and `uv run flask run`. The development server uses SQLite by default and reads `.development.env`.

Does Lin-CMS-Flask include a frontend?

No. It is the Flask backend of the Lin CMS framework, a front end and back end separated CMS. The README points to a separate Vue frontend repository, and the demo site is hosted separately.

What is the difference between `flask db init` and Alembic init?

`flask db init` creates the tables and seeds the data, and internally runs `flask db upgrade`. The native Alembic init, which scaffolds a migration repository, is intentionally shadowed because the `migrations/` directory is committed with the repository.

How do I switch Lin-CMS-Flask from SQLite to MySQL or PostgreSQL?

Install the driver extra with `uv sync --extra mysql` or `uv sync --extra postgres`, then change `SQLALCHEMY_DATABASE_URI` in the environment file. For PostgreSQL, the README warns that since SQLAlchemy 2.1 the default driver for a bare `postgresql://` string is psycopg 3, so psycopg2-binary alone will not match the connection string.

Why does the permission_meta decorator have to sit above the route decorator?

Because it matches permission metadata by function identity, so any decorator that wraps the function, such as a rate limiter, has to sit below it. Getting the order wrong does not raise an error, it just stops the permission from being synchronised into the permission table, and `tests/test_permission_sync.py` catches that in CI.

Official sources

  1. Issues
  2. logamee/lin-cms-flask on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/logamee-lin-cms-flask.svg)](https://hysenlabs.com/projects/logamee-lin-cms-flask)

Was this useful?