# otwcode/otwarchive: the Rails application behind AO3

> otwcode/otwarchive is the Ruby web application behind the Archive of Our Own, a nonprofit fanwork archive run by the Organization for Transformative Works. You get the whole Rails stack, and you inherit the database, the search node and the moderation job.

**otwcode/otwarchive** — The Organization for Transformative Works (OTW) - Archive Of Our Own (AO3) Project

- Repository: https://github.com/otwcode/otwarchive
- Website: https://www.transformativeworks.org/archive_of_our_own
- Stars: 2,278 · Forks: 868
- Language: Ruby
- License: GPL-2.0
- Published: 2026-10-13 · Updated: 2026-10-13 · Language: en
- Canonical page: https://hysenlabs.com/projects/otwcode-otwarchive

## One Rails application behind a multifandom fanwork archive

otwcode/otwarchive is a Ruby web application for hosting archives of fanworks, covering fanfic, fanart and fan vids, and the same codebase runs the Archive of Our Own at archiveofourown.org. The root is a conventional Rails deployment: `config.ru` as the Rack entry point, `Capfile` for Capistrano tasks, `Gemfile` and `Gemfile.lock` for dependencies, then `app/`, `config/`, `db/`, `lib/`, `public/` and `script/`. The Organization for Transformative Works sponsors and manages the project as a nonprofit by and for fans, and the same repository serves the production site and anyone who wants a private copy of it. The code is offered under GPL-2.0-or-later by that organization, so anything deployed from it keeps the same terms and passes its source along, and the last push to master landed on 2026-09-25 with no GitHub releases, so there is no tag to pin a deployment to.

## Three stateful services decide what a local instance can run

Running it locally means running three backing services, and `docker-compose.yml` pins each one: MariaDB at 10.5.4 on the focal tag, Redis 7, and Elasticsearch 9.3.0. Every service keeps its data in a named volume, my-datavolume for MySQL, redis-data and esdata1, so rebuilding containers does not throw the database away.

```yaml
db:
  image: mariadb:10.5.4-focal
  environment:
    - MYSQL_ROOT_PASSWORD=change_me
  ports:
    - "3306:3306"
```

The database publishes 3306 to the host and starts mysqld with a strict sql-mode that names six flags, including NO_AUTO_CREATE_USER and NO_ENGINE_SUBSTITUTION. Redis publishes 6379 and carries no configuration beyond its image tag, which tells you how much of the job is expected to be handled elsewhere.

Elasticsearch gets the longest stanza in the file:

```yaml
redis:
  image: redis:7
es:
  image: docker.elastic.co/elasticsearch/elasticsearch:9.3.0
  ports:
    - "9200:9200"
    - "9300:9300"
    - "9400:9400"
  environment:
    - transport.host=localhost
    - "ES_JAVA_OPTS=-Xms1500m -Xmx1500m"
    - discovery.type=single-node
```

It publishes 9200, 9300 and 9400, sets transport.host=localhost, holds discovery at single-node, and fixes the heap at 1500 megabytes on each side of the equals sign. A transport host of localhost means the node will not talk to a second copy of itself, so duplicating the service changes nothing until you edit that setting. A comment in the same block links issue 78500 to silence the security features are not enabled warning, which is the file admitting the stack is unhardened.

## No install section in the README, setup notes on another site

The README has no installation section. Where a first-run guide would normally sit, it links to setup notes that SquidgeWorld hosts at squidgeworld.org/works/34491, outside the repository and outside anything the project versions itself. Anyone starting from the repository alone has to find the working instructions inside a comment block in `docker-compose.yml`, or open a third-party page and trust what it says. The rest of the README points sideways in the same way: developer documentation lives in the GitHub wiki, and tracked work lives in Jira. That arrangement suits a volunteer project whose own live site is the reference implementation. It slows down anyone who wants a private archive running today.

## Docker init scripts then a web container on port 3000

The compose comments give the shortest real path, and the order matters. One of two init scripts has to run first, and the file names them by platform. That script handles setting up config files, creating the databases and running migrations, so skipping it leaves you without config and without a schema.

```bash
script/docker/init.sh
docker compose up -d web
```

Run `script/docker/init.cmd` on Windows instead, which does the same work in a batch file. After the init step the development webserver comes up in the background and answers on port 3000 at http://localhost:3000/. Before trusting the stack with anything, change `MYSQL_ROOT_PASSWORD=change_me` in the compose file: that literal is checked into a public repository, and port 3306 is published to the host, so an unmodified checkout exposes a database with a password everyone can read. The commented steps in the compose file are the only in-repo instructions, and nothing in those comments says what happens if you run them twice.

## Tests run in the test container, not the web container

The compose file is explicit that tests do not belong in the container serving the page. You are told to use the test container instead, because it includes a headless Chrome container for the JavaScript tests, and you get one worked example:

```bash
docker compose run --rm test bundle exec cucumber features/other_a/autocomplete.feature
```

That command is narrower than it looks. It targets a single feature file, so a full run means enumerating `features/` yourself, and the `--rm` flag deletes the container on the way out, which keeps repeated local runs from piling up containers. The root of the repository holds `spec/`, `test/` and `features/` at the same time, and nothing in the file listing says which of the three a given change belongs to. A new contributor has to read `CONTRIBUTING.md` and the wiki before guessing, and the sample command points at an autocomplete feature, so the suite exercises a user-facing behaviour and not only units.

## Spec, test and features directories share one root

The root also carries an unusually wide set of lint and coverage configuration, which is a fair proxy for how many kinds of file a single change can touch. RuboCop has both `.rubocop.yml` and a `rubocop/` directory. ERB templates are covered by `.erb-lint.yml`, JavaScript by `.jshintrc` and `.jshintignore`, coverage by `.simplecov` and `.codecov.yml`, static analysis by `.codeclimate.yml`, and continuous integration linting by `.hound.yml`. `.phrase.yml` sits in the same group, and the file listing does not say what it governs. `.ruby-version` pins the interpreter and `.devcontainer.json` describes a development container, so the repository offers three different definitions of a working environment inside one directory, and a change to the Ruby version or the container image is a change to how everyone else builds.

## The README rules out API work while linking a live site

The README is direct about one thing: there is currently no API for the OTW-Archive software, and contributors are asked to take API ideas to the Jira issue tracker instead of building them. Put that next to the same file linking archiveofourown.org, a public archive with a large readership, and the gap is the first thing an adopter runs into. A planned integration with your own site has no documented interface to build against, and nothing in the README commits to one arriving on a date. The alternative is not another Rails archive package, it is the hosted service: reading at archiveofourown.org costs no infrastructure at all, while running otwcode/otwarchive on your own hardware trades that convenience for control of the data and full responsibility for moderation, backups and upgrades.

## Contributing runs through Jira after the first pull request

Contribution runs through Jira rather than GitHub issues. Bugs and features are tracked at otwarchive.atlassian.net/projects/AO3/issues, the process is written up in `CONTRIBUTING.md`, and the wiki holds the developer documentation and the commit policy. Commenting on, assigning and transitioning Jira issues become available after your first pull request, which makes that first change the price of admission to the tracker. Review is open to small corrections, including typos, and reviewers receive public credit on the organization's own site. There is no public chat, so questions go to otw-coders@transformativeworks.org. The README lists software and services free to open source projects, among them RubyMine from JetBrains, Hound from thoughtbot, BrowserStack and Sentry, with the rest in `ACKNOWLEDGMENTS.md`.

## Conclusion

otwcode/otwarchive is worth running when a fan group needs its own data and its own moderation rules, and it is the wrong choice for anyone who needs a working archive this week, because the project leaves its first-run instructions to a page hosted elsewhere. Read `docker-compose.yml` before anything else: the placeholder root password, the pinned MariaDB 10.5.4 image and the Elasticsearch node held at single-node discovery each shape what you get. Then accept that there is no API to integrate against, and plan the moderation work yourself.

## FAQ

### Does otwcode/otwarchive have a public API?

No. The README states that there is currently no API for the OTW-Archive software, and asks contributors to take API ideas to the Jira issue tracker instead of building them.

### How do I install otwcode/otwarchive locally?

The README has no installation section and links to setup notes hosted on squidgeworld.org. The compose file names an init script for the platform, `script/docker/init.sh` on Linux or `script/docker/init.cmd` on Windows, which creates config files, databases and migrations, after which `docker compose up -d web` serves the site on port 3000.

### What services does otwcode/otwarchive need before it will run?

Three pinned services from the compose file: MariaDB 10.5.4 on the focal tag, Redis 7, and Elasticsearch 9.3.0, each backed by its own named volume. MySQL publishes 3306, Redis publishes 6379, and Elasticsearch publishes 9200, 9300 and 9400.

### How do I run the otwcode/otwarchive test suite?

Use the test container rather than the web container, because the test container includes a headless Chrome container for the JavaScript tests. The documented example runs cucumber against a single feature file, so a full suite means enumerating the features directory.

### Is otwcode/otwarchive still being worked on?

The repository is not archived, and the last push landed on 2026-09-25. It has no GitHub releases, so there is no tag to deploy from, and upgrades track the master branch rather than a numbered version.

## Sources

- [Issues](https://github.com/otwcode/otwarchive/issues)
- [License: GPL-2.0](https://github.com/otwcode/otwarchive/blob/master/LICENSE)
- [otwcode/otwarchive on GitHub](https://github.com/otwcode/otwarchive)
- [Project website](https://www.transformativeworks.org/archive_of_our_own)
- [README](https://github.com/otwcode/otwarchive/blob/master/README.md)

---

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