# Susun Jadwal backend: a Flask API that scrapes the university course catalog

> susunjadwal-backend is the MIT licensed Python backend for Susun Jadwal, the course planning app built by Ristek Fasilkom UI. It couples a Flask API, MongoDB, a RabbitMQ queue and a scraper for academic.ui.ac.id, which makes it a niche service with a specific institutional shape.

**ristekoss/susunjadwal-backend** — SusunJadwal - Universitas Indonesia's #1 Student Course Planner App.

- Repository: https://github.com/ristekoss/susunjadwal-backend
- Website: http://susunjadwal.cs.ui.ac.id/
- Stars: 861 · Forks: 11
- Language: Python
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/ristekoss-susunjadwal-backend

## One app, one university, one course catalog

Susun Jadwal is described as an open source tool to plan class schedules for university students, developed by Ristek Fasilkom UI, and the repository description calls it Universitas Indonesia's number one student course planner app. The homepage listed in the repository is susunjadwal.cs.ui.ac.id.

The backend therefore serves a very specific job: it turns a university's course catalog into something a student can plan against. That audience is narrow, which is not a criticism so much as a fact to check before adopting anything here. The data it works with has a period shape, the ACTIVE_PERIOD setting in the environment template expects a value such as 2020-2, and the login path goes through SSO UI, the university single sign-on system. A team outside that context would be rebuilding all of that.

What is reusable is the architecture: a small Flask application in front of MongoDB, an asynchronous course update path built on RabbitMQ, a scraper that pulls from the academic portal, and a container image that packages the whole thing for deployment. The repository is MIT licensed, and the README notes that the software goes back a long way with many contributors involved.

## The directory layout splits along data sources

The structure listed in the README maps the service cleanly. The app directory holds general views, models contains the MongoDB document models, scraper holds the courses scraper pointed at academic.ui.ac.id, and sso holds the SSO UI authentication logic. There is also a uploader directory and a widgets directory at the top level of the repository, a requirements.txt with the Python dependency list, and a scripts directory with four shell scripts.

Those scripts describe the whole operational lifecycle in miniature. init-mongo.sh creates a non-root MongoDB user, launch.sh is the main script to start Flask, start.sh is an alternative script to start Flask, and mongo_dump.sh dumps MongoDB data to a .dump file. A cron.sh file sits at the repository root, which suggests scheduled work, and the deployment section points to GitHub Actions workflow files named deploy-<env>.yaml.

The dependency list explains what the service is built from and, just as usefully, what it dragged in. Flask 1.1.1 with flask-mongoengine and mongoengine for persistence, Flask-Cors for browser access, WTForms and Flask-WTF for forms, gunicorn 23.0.0 as the WSGI server, pika 1.2.0 for the RabbitMQ client, and python-cas plus PyJWT for the CAS style SSO flow. Sentry is wired in through sentry-sdk[flask]. Notably, the list also carries playwright>=1.40.0, which matters for the container build.

## MongoDB and RabbitMQ start before the Flask server does

The development setup assumes a Python virtual environment, docker, and Python itself, which the README says was tested on 3.6 and 3.9.18. The order of operations matters because the server talks to both datastores.

The first step brings up MongoDB and RabbitMQ from the development compose file:

```bash
docker-compose -f dev.docker-compose.yaml up
```

The full compose file shows what those two services look like. MongoDB runs from the mongo:7 image with a named volume sunjad_db mounted at /data/db, and RabbitMQ runs from rabbitmq:management-alpine with ports 15671 and 15672 published. The compose file also defines a server service that builds from the local Dockerfile, mounts ./.env read-only at /opt/app/.env:ro, and publishes 8006 on 8006. The server service declares a dependency on both rmq and mongo, so container start order is handled for you.

The Flask server itself is started by a shell script rather than by compose in the local flow:

```bash
export PORT=8000 && bash scripts/launch.sh
```

With the server up, the README tells you to ping the API at http://localhost:8000/susunjadwal/api/. Note the port difference between the two flows. Local development uses 8000 through the exported PORT, while the container image sets PORT to 8006 and the compose file publishes 8006 on the host.

## The .env file is where the institution specific settings live

Step two of the setup is to populate .env using .env.example as a reference, and the template is short enough to read in full. It holds a SECRET_KEY, the ACTIVE_PERIOD in a form like 2020-2, an SSO_UI_FORCE_HTTPS flag, the RabbitMQ exchange name and credentials, the MongoDB connection details, a Sentry DSN, and an ADMIN_CREDENTIAL_VERIFICATION string.

The template uses a deliberate convention for hosts that differ between the two environments. RABBIT_HOST is written as rmq || localhost and MONGODB_HOST as mongo || localhost, which is what lets the same .env file serve both the containerised stack and a local run against services on the same machine.

The MongoDB section names the port explicitly at 27017, and the credentials map to the MONGO_INITDB_USERNAME and MONGO_INITDB_PASSWORD values in the compose file. Those compose defaults are the placeholder pair root-user for the root account and user for the application account. Setting MONGO_INITDB_ROOT_USERNAME and MONGO_INITDB_ROOT_PASSWORD is the first thing to change before any deployment, since the shipped values are obviously not secrets.

Creating the application user is a manual step, not a compose side effect. The README shows connecting with mongosh as the root user, switching to the database, and running a createUser call with the readWrite role, then expecting a response of {ok: 1} and copying those credentials into the .env secrets.

## The container image installs Playwright Chromium at build time

The Dockerfile is where the service's hidden weight shows up. It starts from python:3.12-slim and copies the repository into /opt/app, then copies scripts/launch.sh plus three JSON files from the sso directory into the image. The apt-get stage installs ca-certificates, tzdata, gcc, libxml2-dev, libxslt-dev and a long list of browser libraries including libnss3, libgtk-3-0, libasound2 and libgbm1.

Those browser libraries are there for Playwright. The build runs pip install -r requirements.txt, then pip install --no-cache-dir playwright, then python -m playwright install chromium --with-deps || true. The trailing || true means a failed Chromium download does not fail the image build, which is a deliberate tradeoff: the image still builds on a machine where the browser download is blocked, but the scraper will not work at runtime.

The entrypoint is /bin/sh launch.sh, and APP_ENV is set to container with ENV PORT=8006. So the container and the script share one start path.

The deployment note is short: replace lines 23 to 25 of docker-compose.yml to pull from Docker Hub instead of building locally.

```yaml
image: ristekoss/susunjadwal-backend:stable
```

After that the MONGO_INITDB_XX credentials are modified and the stack starts with docker compose up.

## Backup and restore happen through files copied into the container

The database side has its own documented procedure, and it is the part most likely to be used by an engineer taking over the service. The dump is one command:

```bash
bash mongo_dump.sh
```

The result is a .dump file in the ./mongodump directory. Restoring takes three steps, and the first one is a file copy into the running MongoDB container:

```bash
docker cp <path_to_dump_file> susunjadwalbackend_mongo_1:/<path_to_dump_file>
```

The container name in that command reflects the default compose naming, so a renamed project or a different compose file means adjusting it. The second step loads the archive:

```bash
docker exec -it <mongo_container_name> mongorestore -u <root_username> --archive=<file.dump>
```

The password is entered when prompted, and a successful run reports XX documents successfully restored. The procedure is a file based one, so a restore depends on having a dump file on the machine running the command. There is no scripted path for dumping straight to object storage or to a remote host.

That is also the honest comparison point. A team that needs continuous, automated backups with retention and verification has to build those pieces around this service, because what the repository gives you is a shell script and a copy command, not a backup system.

## No tagged releases, and the pre-2023 code lives in a separate tree

The repository has no GitHub releases at all. It is not archived, and the last push was on 2026-09-23. That combination tells you the project is still being worked on while offering no versioned artefacts to pin against, so every deployment tracks whatever the branch happens to contain.

The deployment process uses a standardized pipeline invoked from workflow files named deploy-<env>.yaml in the .github directory, which is the internal mechanism used across the organisation's products. The README also points to a legacy tree at commit 5b8f710 for the version of susunjadwal-backend that was maintained and deployed up until 2023. That split is worth knowing about, because a bug report against current master may not reproduce on a deployment that is still running the older tree.

The upgrade cost is therefore shaped by infrastructure rather than by a version bump. Moving a running deployment forward means rebuilding the image, since the compose file builds from the local Dockerfile unless you switch to the stable image, and rebuilding means re-downloading Playwright Chromium. Anyone planning that work should read Dockerfile and docker-compose.yaml side by side first.

## Conclusion

Adopt susunjadwal-backend if you are contributing to or hosting Susun Jadwal itself, since the scraper, the SSO module and the queue are all coupled to that one deployment. Do not adopt it as a general course scheduling service, because the academic.ui.ac.id scraper, the UI SSO login flow and the Indonesian academic period in ACTIVE_PERIOD are hard coded assumptions. Before running it, populate .env from .env.example, create the non-root MongoDB user described in the setup steps, and confirm that scripts/launch.sh starts the Flask app on the PORT you export.

## FAQ

### How do I run the susunjadwal-backend locally?

The README requires Python with pip, tested on 3.6 and 3.9.18, plus docker. Start MongoDB and RabbitMQ with docker-compose -f dev.docker-compose.yaml up, populate .env from .env.example, create a non-root MongoDB user, then start Flask with export PORT=8000 && bash scripts/launch.sh and ping http://localhost:8000/susunjadwal/api/.

### What does the susunjadwal-backend .env file need to contain?

The .env.example template lists SECRET_KEY, ACTIVE_PERIOD, SSO_UI_FORCE_HTTPS, the RabbitMQ exchange name and credentials, MONGODB_DB, MONGODB_HOST, MONGODB_PORT, MONGODB_USERNAME, MONGODB_PASSWORD, SENTRY_DSN and ADMIN_CREDENTIAL_VERIFICATION. Hosts are written as rmq || localhost and mongo || localhost so the same file works in both flows.

### Which datastores does susunjadwal-backend depend on?

The compose file defines MongoDB from the mongo:7 image with a named volume sunjad_db, and RabbitMQ from rabbitmq:management-alpine with ports 15671 and 15672. The server service mounts ./.env read-only at /opt/app/.env:ro and publishes port 8006.

### How do I back up and restore the Susun Jadwal database?

Run bash mongo_dump.sh, which writes a .dump file into ./mongodump. To restore, copy the file into the container with docker cp, then run docker exec -it <mongo_container_name> mongorestore -u <root_username> --archive=<file.dump> and enter the root password when prompted.

### Does susunjadwal-backend have versioned releases to pin against?

No, the repository has no GitHub releases. The last push was on 2026-09-23, and the code that was maintained and deployed up until 2023 is kept in a separate legacy tree referenced at commit 5b8f710.

## Sources

- [Issues](https://github.com/ristekoss/susunjadwal-backend/issues)
- [License: MIT](https://github.com/ristekoss/susunjadwal-backend/blob/master/LICENSE)
- [Project website](http://susunjadwal.cs.ui.ac.id/)
- [README](https://github.com/ristekoss/susunjadwal-backend/blob/master/README.md)
- [ristekoss/susunjadwal-backend on GitHub](https://github.com/ristekoss/susunjadwal-backend)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ristekoss-susunjadwal-backend
