I maintain PortalJS, an open source framework for building data portals, and the most common situation we see is this: an organization runs CKAN, the data management side works fine, and everyone hates the website.
That is not CKAN's fault. CKAN is twenty years of solid dataset management: harvesting, organizations, permissions, a real API. But its frontend is a server-rendered Jinja theme, and customizing it means learning the CKAN extension system, template overrides, and a deployment story that makes frontend developers quietly leave the room. So teams either live with a dated portal or budget a five-figure theming project that still ends up coupled to a specific CKAN version.
The fix we settled on is boring and old: treat CKAN the way you already treat your database. Nobody renders HTML out of Postgres. You put an application in front of it and talk to it over an API.
The seam
PortalJS portals are plain Next.js. Every page reads data through one interface, a DataProvider. There is a provider that talks to the CKAN API, and there are providers for static files, GitHub, and Frictionless data packages, with OpenMetadata and others on the roadmap. The pages do not know or care which one is behind them.
Concretely, a portal is three surfaces: a Home page, a Catalog at /search, and a dataset Showcase at /@namespace/slug with metadata, preview, download links, and charts or maps. All three read through the provider. Point the provider at your CKAN instance and your existing datasets, organizations, and search show up in a modern frontend. CKAN keeps doing everything it is good at. You never touch a Jinja template again, and your frontend team works in the stack they already know.
The practical consequences are the reason we built it this way:
Your CKAN upgrade path and your frontend release cycle stop being the same project. You can redesign the portal without touching the data layer, and upgrade CKAN without redeploying the website.
Migration stops being a cliff. If you later move off CKAN, to plain files and object storage, or the other direction into a heavier catalog, you swap the provider and keep every page.
You can run it alongside what exists today. Several teams run the new frontend on a subdomain against production CKAN while the old portal keeps serving traffic. Coexistence first, cutover when ready.
Where this goes beyond open data
The same decoupling matters inside companies. An internal data portal is the same three surfaces over whatever you already run: CKAN, DataHub, OpenMetadata, or a directory of Parquet files. The catalog your data platform team maintains does not need to be the UI your analysts and, increasingly, your AI agents use to find data. A frontend with one provider contract in front of it is a much smaller commitment than migrating catalogs.
The AI part, briefly
PortalJS ships Claude Code skills that do the assembly. In practice, wiring a portal to CKAN looks like:
/portaljs-new-portal "City of Kyle open data portal"
/portaljs-connect-ckan https://data.kyle-tx.gov
There is also /portaljs-migrate, which harvests a whole catalog from CKAN, Socrata, OpenDataSoft, ArcGIS, or a DCAT-US feed if you want to move the data rather than sit in front of it. The skills are a convenience, not a requirement. Everything they generate is plain Next.js you can write by hand, and there is a bare template if you want no AI in the loop at all.
Honest limitations
The CKAN provider covers the read paths a portal needs (search, dataset pages, organizations). It is not an admin UI; publishing and permissions still happen in CKAN itself. There is no generic harvester for arbitrary DCAT endpoints yet, and the skills currently target Claude Code specifically. Roadmap is public in the repo if you want to see where this is going, and issues are very welcome.
Repo: https://github.com/datopian/portaljs
If you run CKAN and have opinions about where this model falls over, I would genuinely like to hear them. The decoupled bet only works if the provider contract survives contact with real deployments.
Top comments (0)