diff --git a/package-lock.json b/package-lock.json
index 1ce79bb2dc8d4d8557caa3e2f73f46de4c4992d2..445a602d57c899a8e98f013ba74f05382363076a 100644
Binary files a/package-lock.json and b/package-lock.json differ
diff --git a/package.json b/package.json
index d6ddc61c460c78fca42b1210eedad59b287efd88..3185d9554462f5b524b2e682a7c2708e9e305a68 100644
--- a/package.json
+++ b/package.json
@@ -24,15 +24,20 @@
     "transform": {
       "\\.ts$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
     },
-    "moduleDirectories": ["node_modules", "build/node_modules"], 
+    "moduleDirectories": [
+      "node_modules",
+      "build/node_modules"
+    ],
     "testRegex": "\\.spec\\.ts$"
   },
   "author": "",
   "license": "MIT",
   "devDependencies": {
     "@types/benchmark": "^1.0.30",
+    "@types/express": "^4.0.39",
     "@types/jest": "^21.1.5",
     "@types/node": "^8.0.47",
+    "@types/node-fetch": "^1.6.7",
     "benchmark": "^2.1.4",
     "download-cli": "^1.0.5",
     "jest": "^21.2.1",
@@ -45,8 +50,11 @@
     "ts-jest": "^21.1.4",
     "tslint": "^5.8.0",
     "typescript": "^2.6.1",
-    "uglify-js": "^3.1.6",
+    "uglify-js": "^3.1.7",
     "util.promisify": "^1.0.0"
   },
-  "dependencies": {}
+  "dependencies": {
+    "express": "^4.16.2",
+    "node-fetch": "^1.7.3"
+  }
 }
diff --git a/src/apps/domain-annotation-server/mapping.ts b/src/apps/domain-annotation-server/mapping.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4489cb959ff608efb62c99c89081cdf1fccc1f05
--- /dev/null
+++ b/src/apps/domain-annotation-server/mapping.ts
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Table } from 'mol-base/collections/database'
+import { CIFEncoder, create as createEncoder } from 'mol-io/writer/cif'
+import * as S from './schemas'
+import { getCategoryInstanceProvider } from './utils'
+
+export default function create(allData: any) {
+    const mols = Object.keys(allData);
+    if (!mols.length) return '#';
+
+    const data = allData[mols[0]];
+
+    const enc = createEncoder();
+    enc.startDataBlock(mols[0]);
+
+    for (const cat of Object.keys(S.categories)) {
+        writeDomain(enc, getDomain(cat, (S.categories as any)[cat], data));
+    }
+    return enc.getData();
+}
+
+interface DomainAnnotation {
+    name: string,
+    domains: Table<any>,
+    mappings: Table<S.mapping>
+}
+type MappingRow = Table.Row<S.mapping>;
+
+function writeDomain(enc: CIFEncoder<any>, domain: DomainAnnotation | undefined) {
+    if (!domain) return;
+    enc.writeCategory(getCategoryInstanceProvider(`pdbx_${domain.name}_domain_annotation`, domain.domains));
+    enc.writeCategory(getCategoryInstanceProvider(`pdbx_${domain.name}_domain_mapping`, domain.mappings));
+}
+
+function getMappings(startId: number, group_id: number, mappings: any): MappingRow[] {
+    const rows: MappingRow[] = [];
+
+    const n = (v: any) => v === null ? void 0 : v;
+
+    for (const entry of mappings) {
+        if (entry.start && entry.end) {
+            rows.push({
+                id: startId++,
+                group_id,
+                label_entity_id: '' + entry.entity_id,
+                label_asym_id: entry.struct_asym_id,
+                auth_asym_id: entry.chain_id,
+                beg_label_seq_id: n(entry.start.residue_number),
+                beg_auth_seq_id: n(entry.start.author_residue_number),
+                pdbx_beg_PDB_ins_code: entry.start.author_insertion_code,
+                end_label_seq_id: n(entry.end.residue_number),
+                end_auth_seq_id: n(entry.end.author_residue_number),
+                pdbx_end_PDB_ins_code: entry.end.author_insertion_code
+            });
+        } else {
+            rows.push({
+                id: startId++,
+                group_id,
+                label_entity_id: '' + entry.entity_id,
+                label_asym_id: entry.struct_asym_id,
+                auth_asym_id: entry.chain_id
+            } as any);
+        }
+    }
+    return rows;
+}
+
+function getDomainInfo(id: string, mapping_group_id: number, data: any, schema: any) {
+    const props = Object.create(null);
+    for (const k of Object.keys(schema)) props[k] = data[k];
+    return { id, mapping_group_id, identifier: data.identifier, ...props };
+}
+
+function getDomain(name: string, schema: any, allData: any) {
+    if (!allData[name]) return void 0;
+
+    const data = allData[name];
+
+    const domains: any[] = [];
+    const mappings: MappingRow[] = [];
+
+    let mappingSerialId = 1, mapping_group_id = 1;
+
+    for (const id of Object.keys(data)) {
+        const domain = data[id];
+        domains.push(getDomainInfo(id, mapping_group_id, domain, schema));
+        mappings.push(...getMappings(mappingSerialId, mapping_group_id, domain.mappings));
+        mappingSerialId = mappings.length + 1;
+        mapping_group_id++;
+    }
+
+    return domains.length > 0 ? {
+        name,
+        domains: Table.ofRows({ ...S.Base, ...schema }, domains),
+        mappings: Table.ofRows(S.mapping, mappings)
+    } : void 0;
+}
\ No newline at end of file
diff --git a/src/apps/domain-annotation-server/schemas.ts b/src/apps/domain-annotation-server/schemas.ts
new file mode 100644
index 0000000000000000000000000000000000000000..370bf1786499af3c02185619c4acbe5052934d37
--- /dev/null
+++ b/src/apps/domain-annotation-server/schemas.ts
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Column } from 'mol-base/collections/database'
+
+import Type = Column.Type
+
+export const Base = {
+    id: Type.str,
+    identifier: Type.str,
+    mapping_group_id: Type.int
+}
+export type Base = typeof Base
+
+export const mapping = {
+    id: Type.int,
+    group_id: Type.int,
+
+    label_entity_id: Type.str,
+    label_asym_id: Type.str,
+    auth_asym_id: Type.str,
+
+    beg_label_seq_id: Type.int,
+    beg_auth_seq_id: Type.int,
+    pdbx_beg_PDB_ins_code: Type.str,
+
+    end_label_seq_id: Type.int,
+    end_auth_seq_id: Type.int,
+    pdbx_end_PDB_ins_code: Type.str
+}
+export type mapping = typeof mapping
+
+export const Pfam = {
+    description: Type.str
+}
+export type Pfam = typeof Pfam
+
+export const InterPro = {
+    name: Type.str
+}
+export type InterPro = typeof InterPro
+
+export const CATH = {
+    name: Type.str,
+    homology: Type.str,
+    architecture: Type.str,
+    identifier: Type.str,
+    class: Type.str,
+    topology: Type.str,
+}
+export type CATH = typeof CATH
+
+export const EC = {
+    accepted_name: Type.str,
+    reaction: Type.str,
+    systematic_name: Type.str
+}
+export type EC = typeof EC
+
+export const UniProt = {
+    name: Type.str
+}
+export type UniProt = typeof UniProt
+
+export const SCOP = {
+    sccs: Type.str,
+    description: Type.str
+}
+export type SCOP = typeof SCOP
+
+export const GO = {
+    category: Type.str,
+    definition: Type.str,
+    name: Type.str
+}
+export type GO = typeof GO
+
+export const categories = {
+    Pfam,
+    InterPro,
+    CATH,
+    EC,
+    UniProt,
+    SCOP,
+    GO
+}
\ No newline at end of file
diff --git a/src/apps/domain-annotation-server/server.ts b/src/apps/domain-annotation-server/server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1d4f192cad0713c96b559e427535986532c8dc54
--- /dev/null
+++ b/src/apps/domain-annotation-server/server.ts
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as express from 'express'
+import fetch from 'node-fetch'
+import createMapping from './mapping'
+
+async function getMappings(id: string) {
+    const data = await fetch(`https://www.ebi.ac.uk/pdbe/api/mappings/${id}`);
+    const json = await data.json();
+    return createMapping(json);
+};
+
+
+let PORT = process.env.port || 1338;
+
+const app = express();
+
+const PREFIX = '/'
+
+app.get(`${PREFIX}/:id`, async (req, res) => {
+    try {
+        console.log('Requesting ' + req.params.id);
+        const mapping = await getMappings(req.params.id);
+        res.writeHead(200, {
+            'Content-Type': 'text/plain; charset=utf-8',
+            'Access-Control-Allow-Origin': '*',
+            'Access-Control-Allow-Headers': 'X-Requested-With'
+        });
+        res.end(mapping);
+    } catch {
+        console.log('Failed ' + req.params.id);
+        res.writeHead(404, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'X-Requested-With' });
+        res.end();
+    }
+});
+
+app.get(`${PREFIX}`, (req, res) => {
+    res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
+    res.end('Usage: /pdb_id, e.g. /1tqn');
+})
+
+app.listen(PORT);
+
+console.log('Running on port ' + PORT);
\ No newline at end of file
diff --git a/src/apps/domain-annotation-server/test.ts b/src/apps/domain-annotation-server/test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..66911dec6ce7484f2bd24bc3784b126cbe974259
--- /dev/null
+++ b/src/apps/domain-annotation-server/test.ts
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import fetch from 'node-fetch'
+import createMapping from './mapping'
+
+(async function () {
+    const data = await fetch('https://www.ebi.ac.uk/pdbe/api/mappings/1tqn?pretty=true');
+    const json = await data.json();
+    console.log(createMapping(json));
+}());
\ No newline at end of file
diff --git a/src/apps/domain-annotation-server/utils.ts b/src/apps/domain-annotation-server/utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cb5c6badddce091441def0b26f8ab6b8c552f247
--- /dev/null
+++ b/src/apps/domain-annotation-server/utils.ts
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Table } from 'mol-base/collections/database'
+import Iterator from 'mol-base/collections/iterator'
+import * as Encoder from 'mol-io/writer/cif'
+
+function columnValue(k: string) {
+    return (i: number, d: any) => d[k].value(i);
+}
+
+function columnValueKind(k: string) {
+    return (i: number, d: any) => d[k].valueKind(i);
+}
+
+function ofSchema(schema: Table.Schema) {
+    const fields: Encoder.FieldDefinition[] = [];
+    for (const k of Object.keys(schema)) {
+        const t = schema[k];
+        // TODO: matrix/vector/support
+        const type = t.kind === 'str' ? Encoder.FieldType.Str : t.kind === 'int' ? Encoder.FieldType.Int : Encoder.FieldType.Float;
+        fields.push({ name: k, type, value: columnValue(k), valueKind: columnValueKind(k) })
+    }
+    return fields;
+}
+
+function ofTable<S extends Table.Schema>(name: string, table: Table<S>): Encoder.CategoryDefinition<number> {
+    return { name, fields: ofSchema(table._schema) }
+}
+
+export function getCategoryInstanceProvider(name: string, table: Table<any>): Encoder.CategoryProvider {
+    return () => {
+        return {
+            data: table,
+            definition: ofTable(name, table),
+            keys: () => Iterator.Range(0, table._rowCount - 1),
+            rowCount: table._rowCount
+        };
+    }
+}
diff --git a/src/mol-io/writer/cif/encoder.ts b/src/mol-io/writer/cif/encoder.ts
index 64a18622ff509c86fe1ae394c239b9bf7915f898..f32bec34bf5506c7efff06723630e89980e5bea7 100644
--- a/src/mol-io/writer/cif/encoder.ts
+++ b/src/mol-io/writer/cif/encoder.ts
@@ -57,7 +57,7 @@ export interface CategoryProvider {
     (ctx: any): CategoryInstance
 }
 
-export interface CIFEncoder<T, Context> extends Encoder<T> {
+export interface CIFEncoder<T = string | Uint8Array, Context = any> extends Encoder<T> {
     startDataBlock(header: string): void,
     writeCategory(category: CategoryProvider, contexts?: Context[]): void,
     getData(): T