diff --git a/src/mol-model/structure/model.ts b/src/mol-model/structure/model.ts
index c208d036d49c19603f1b93bf8960412644c44def..05dec32e216ca63c83ad5f5e80767f027fef847b 100644
--- a/src/mol-model/structure/model.ts
+++ b/src/mol-model/structure/model.ts
@@ -10,5 +10,6 @@ import Format from './model/format'
 import { ModelSymmetry } from './model/properties/symmetry'
 import StructureSequence from './model/properties/sequence'
 
+export * from './model/properties/custom'
 export * from './model/indexing'
 export { Model, Types, Format, ModelSymmetry, StructureSequence }
\ No newline at end of file
diff --git a/src/mol-model/structure/model/properties/utils/atomic-keys.ts b/src/mol-model/structure/model/properties/utils/atomic-keys.ts
index de4029984236b1800a01158a2c37641cd5c8c48d..a13469217300b5881e24724af6fae6d01a0553c2 100644
--- a/src/mol-model/structure/model/properties/utils/atomic-keys.ts
+++ b/src/mol-model/structure/model/properties/utils/atomic-keys.ts
@@ -10,7 +10,9 @@ import { Entities } from '../common'
 import { ChainIndex, ResidueIndex, EntityIndex } from '../../indexing';
 
 function getResidueId(comp_id: string, seq_id: number, ins_code: string) {
-    return `${comp_id} ${seq_id} ${ins_code}`;
+    // TODO: add new index that support comp_id again?
+    return `${seq_id} ${ins_code}`;
+    //return `${comp_id} ${seq_id} ${ins_code}`;
 }
 
 function getElementKey(map: Map<string, number>, key: string, counter: { index: number }) {
diff --git a/src/mol-model/structure/model/properties/utils/coarse-ranges.ts b/src/mol-model/structure/model/properties/utils/coarse-ranges.ts
index 6b0891a48e88542649588c9effae00a63ba1f0f8..0df3abc664cc0d098e0823d7ec78643f48b6704d 100644
--- a/src/mol-model/structure/model/properties/utils/coarse-ranges.ts
+++ b/src/mol-model/structure/model/properties/utils/coarse-ranges.ts
@@ -13,7 +13,7 @@ import { ElementIndex } from '../../indexing';
 // TODO assumes all coarse elements are part of a polymer
 // TODO add gaps at the ends of the chains by comparing to the polymer sequence data
 
-export function getCoarseRanges(data: CoarseElementData, chemicalComponentMap: Map<string, ChemicalComponent>): CoarseRanges {
+export function getCoarseRanges(data: CoarseElementData, chemicalComponentMap: ReadonlyMap<string, ChemicalComponent>): CoarseRanges {
     const polymerRanges: number[] = []
     const gapRanges: number[] = []
     const chainIt = Segmentation.transientSegments(data.chainElementSegments, Interval.ofBounds(0, data.count))
diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts
index 1e806d8d8ec5e1514815a62c322478a6db2924c4..73cd16ee0b9c7cb62fb1fc7bc2ba9e84fe2b2769 100644
--- a/src/mol-model/structure/structure/structure.ts
+++ b/src/mol-model/structure/structure/structure.ts
@@ -18,6 +18,7 @@ import { InterUnitBonds, computeInterUnitBonds } from './unit/links';
 import { CrossLinkRestraints, extractCrossLinkRestraints } from './unit/pair-restraints';
 import StructureSymmetry from './symmetry';
 import StructureProperties from './properties';
+import { ResidueIndex } from '../model/indexing';
 
 class Structure {
     readonly unitMap: IntMap<Unit>;
@@ -278,6 +279,25 @@ namespace Structure {
         sortArray(keys.array);
         return keys.array;
     }
+
+    export function getUniqueAtomicResidueIndices(structure: Structure, model: Model): ReadonlyArray<ResidueIndex> {
+        const uniqueResidues = UniqueArray.create<ResidueIndex, ResidueIndex>();
+        const unitGroups = structure.unitSymmetryGroups;
+        for (const unitGroup of unitGroups) {
+            const unit = unitGroup.units[0];
+            if (unit.model !== model || !Unit.isAtomic(unit)) {
+                continue;
+            }
+
+            const residues = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
+            while (residues.hasNext) {
+                const seg = residues.move();
+                UniqueArray.add(uniqueResidues, seg.index, seg.index);
+            }
+        }
+        sortArray(uniqueResidues.array);
+        return uniqueResidues.array;
+    }
 }
 
 export default Structure
\ No newline at end of file
diff --git a/src/servers/model/properties.ts b/src/servers/model/properties.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3f35e24159d8b3f70cece47a93908a908263d5bb
--- /dev/null
+++ b/src/servers/model/properties.ts
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Model } from 'mol-model/structure';
+import { StructureQualityReport } from './properties/structure-quality-report';
+
+export async function attachModelProperties(model: Model) {
+    await StructureQualityReport.attachFromPDBeApi(model);
+}
\ No newline at end of file
diff --git a/src/servers/model/properties/structure-quality-report.ts b/src/servers/model/properties/structure-quality-report.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0f57a68552a9583f7ebafc49eb3b1fda5e7946c4
--- /dev/null
+++ b/src/servers/model/properties/structure-quality-report.ts
@@ -0,0 +1,124 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ResidueIndex, ModelPropertyDescriptor, Model, Structure, Unit, StructureElement, StructureProperties as P  } from 'mol-model/structure';
+import fetch from 'node-fetch';
+import { CifWriter } from 'mol-io/writer/cif';
+import CifField = CifWriter.Field;
+import { Segmentation } from 'mol-data/int';
+
+type IssueMap = Map<ResidueIndex, string[]>
+
+const _Descriptor: ModelPropertyDescriptor = {
+    isStatic: true,
+    name: 'structure_quality_report',
+    cifExport: {
+        categories: [{
+            name: 'structure_quality_report',
+            instance(ctx) {
+                const issues = StructureQualityReport.get(ctx.model);
+                if (!issues) return CifWriter.Category.Empty;
+
+                const residues = getResidueLoci(ctx.structure, issues);
+                return {
+                    fields: _structure_quality_report_fields,
+                    data: <ExportCtx>{ model: ctx.model, residues, residueIndex: ctx.model.atomicHierarchy.residueAtomSegments.index, issues },
+                    rowCount: residues.length
+                };
+            }
+        }]
+    }
+}
+
+type ExportCtx = { model: Model, residueIndex: ArrayLike<ResidueIndex>, residues: StructureElement[], issues: IssueMap };
+
+const _structure_quality_report_fields: CifField<ResidueIndex, ExportCtx>[] = [
+    CifField.str<ResidueIndex, ExportCtx>('label_comp_id', (i, d) => P.residue.label_comp_id(d.residues[i])),
+    CifField.int<ResidueIndex, ExportCtx>('label_seq_id', (i, d) => P.residue.label_seq_id(d.residues[i])),
+    CifField.str<ResidueIndex, ExportCtx>('pdbx_PDB_ins_code', (i, d) => P.residue.pdbx_PDB_ins_code(d.residues[i])),
+    CifField.str<ResidueIndex, ExportCtx>('label_asym_id', (i, d) => P.chain.label_asym_id(d.residues[i])),
+    CifField.str<ResidueIndex, ExportCtx>('label_entity_id', (i, d) => P.entity.id(d.residues[i])),
+
+    CifField.str<ResidueIndex, ExportCtx>('auth_comp_id', (i, d) => P.residue.auth_comp_id(d.residues[i])),
+    CifField.int<ResidueIndex, ExportCtx>('auth_seq_id', (i, d) => P.residue.auth_seq_id(d.residues[i])),
+    CifField.str<ResidueIndex, ExportCtx>('auth_asym_id', (i, d) => P.chain.auth_asym_id(d.residues[i])),
+
+
+    CifField.str<ResidueIndex, ExportCtx>('issues', (i, d) => d.issues.get(d.residueIndex[d.residues[i].element])!.join(','))
+];
+
+function getResidueLoci(structure: Structure, issues: IssueMap) {
+    const seenResidues = new Set<ResidueIndex>();
+    const unitGroups = structure.unitSymmetryGroups;
+    const loci: StructureElement[] = [];
+
+    for (const unitGroup of unitGroups) {
+        const unit = unitGroup.units[0];
+        if (!Unit.isAtomic(unit)) {
+            continue;
+        }
+
+        const residues = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
+        while (residues.hasNext) {
+            const seg = residues.move();
+            if (!issues.has(seg.index) || seenResidues.has(seg.index)) continue;
+
+            seenResidues.add(seg.index);
+            loci[loci.length] = StructureElement.create(unit, unit.elements[seg.start]);
+        }
+    }
+
+    loci.sort((x, y) => x.element - y.element);
+    return loci;
+}
+
+function createIssueMap(modelData: Model, data: any): IssueMap | undefined {
+    const ret = new Map<ResidueIndex, string[]>();
+    if (!data.molecules) return;
+
+    for (const entity of data.molecules) {
+        const entity_id = entity.entity_id.toString();
+        for (const chain of entity.chains) {
+            const asym_id = chain.struct_asym_id.toString();
+            for (const model of chain.models) {
+                const model_id = model.model_id.toString();
+                if (+model_id !== modelData.modelNum) continue;
+
+                for (const residue of model.residues) {
+                    const auth_seq_id = residue.author_residue_number, ins_code = residue.author_insertion_code || '';
+                    const idx = modelData.atomicHierarchy.findResidueKey(entity_id, asym_id, '', auth_seq_id, ins_code);
+                    ret.set(idx, residue.outlier_types);
+                }
+            }
+        }
+    }
+
+    return ret;
+}
+
+export namespace StructureQualityReport {
+    export const Descriptor = _Descriptor;
+
+    export async function attachFromPDBeApi(model: Model) {
+        if (model.customProperties.has(Descriptor)) return true;
+
+        const id = model.label.toLowerCase();
+        const rawData = await fetch(`https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${model.label.toLowerCase()}`);
+        const json = await rawData.json();
+        const data = json[id];
+        if (!data) return false;
+        const issueMap = createIssueMap(model, data);
+        if (!issueMap || issueMap.size === 0) return false;
+
+        model.customProperties.add(Descriptor);
+        model._staticPropertyData.__StructureQualityReport__ = issueMap;
+        return true;
+    }
+
+    export function get(model: Model): IssueMap | undefined {
+        return model._staticPropertyData.__StructureQualityReport__;
+    }
+}
\ No newline at end of file
diff --git a/src/servers/model/server/query.ts b/src/servers/model/server/query.ts
index a134c16826c518590f75535f5280cc365d49f29a..92b01d6221c6f140f6ad0c46553985a1b2f4ffb4 100644
--- a/src/servers/model/server/query.ts
+++ b/src/servers/model/server/query.ts
@@ -116,6 +116,7 @@ const _model_server_error_fields: CifField<number, string>[] = [
 const _model_server_stats_fields: CifField<number, Stats>[] = [
     int32<Stats>('io_time_ms', ctx => ctx.structure.info.readTime | 0),
     int32<Stats>('parse_time_ms', ctx => ctx.structure.info.parseTime | 0),
+    int32<Stats>('attach_props_time_ms', ctx => ctx.structure.info.attachPropsTime | 0),
     int32<Stats>('create_model_time_ms', ctx => ctx.structure.info.createModelTime | 0),
     int32<Stats>('query_time_ms', ctx => ctx.queryTimeMs | 0),
     int32<Stats>('encode_time_ms', ctx => ctx.encodeTimeMs | 0)
diff --git a/src/servers/model/server/structure-wrapper.ts b/src/servers/model/server/structure-wrapper.ts
index 72bc68992d12d99716441beb557ab8ad100d6c38..d70a75823081a3b387d309779cb83808e22bb089 100644
--- a/src/servers/model/server/structure-wrapper.ts
+++ b/src/servers/model/server/structure-wrapper.ts
@@ -14,6 +14,7 @@ import * as fs from 'fs'
 import * as zlib from 'zlib'
 import { Job } from './jobs';
 import { ConsoleLogger } from 'mol-util/console-logger';
+import { attachModelProperties } from '../properties';
 
 require('util.promisify').shim();
 
@@ -27,6 +28,7 @@ export interface StructureInfo {
     readTime: number;
     parseTime: number;
     createModelTime: number;
+    attachPropsTime: number;
 
     sourceId: string,
     entryId: string
@@ -104,6 +106,10 @@ async function readStructure(key: string, sourceId: string, entryId: string) {
     const models = await Model.create(Format.mmCIF(frame)).run();
     perf.end('createModel');
 
+    perf.start('attachProps');
+    await attachModelProperties(models[0]);
+    perf.end('attachProps');
+
     const structure = Structure.ofModel(models[0]);
 
     const ret: StructureWrapper = {
@@ -112,6 +118,7 @@ async function readStructure(key: string, sourceId: string, entryId: string) {
             readTime: perf.time('read'),
             parseTime: perf.time('parse'),
             createModelTime: perf.time('createModel'),
+            attachPropsTime: perf.time('attachProps'),
             sourceId,
             entryId
         },